Merge "Snap for 7024209 from 0cb6eaf6fa71c30e71fdeb9076d0ce7966ed75a1 to androidx-master-release" into androidx-master-release
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 7fe96b8..4175155 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -30,16 +30,16 @@
     api("androidx.core:core-ktx:1.1.0") {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx")) {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01")
-    api("androidx.savedstate:savedstate-ktx:1.1.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
+    api(projectOrArtifact(":savedstate:savedstate-ktx")) {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
     api(KOTLIN_STDLIB)
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.0-beta01")
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(JUNIT)
     androidTestImplementation(TRUTH)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index b0d8662..1159550 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -23,12 +23,13 @@
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.core:core:1.1.0")
-    api("androidx.lifecycle:lifecycle-runtime:2.3.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.3.0-beta01")
-    api("androidx.savedstate:savedstate:1.1.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+    api(projectOrArtifact(":savedstate:savedstate"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+    implementation("androidx.tracing:tracing:1.0.0")
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.0-beta01")
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(KOTLIN_STDLIB)
     androidTestImplementation(LEAKCANARY)
     androidTestImplementation(LEAKCANARY_INSTRUMENTATION)
diff --git a/activity/activity/src/androidTest/AndroidManifest.xml b/activity/activity/src/androidTest/AndroidManifest.xml
index 8ad02b8..5176eb8 100644
--- a/activity/activity/src/androidTest/AndroidManifest.xml
+++ b/activity/activity/src/androidTest/AndroidManifest.xml
@@ -23,6 +23,7 @@
         <activity android:name="androidx.activity.LifecycleComponentActivity"/>
         <activity android:name="androidx.activity.EagerOverrideLifecycleComponentActivity"/>
         <activity android:name="androidx.activity.LazyOverrideLifecycleComponentActivity"/>
+        <activity android:name="androidx.activity.ReportFullyDrawnActivity"/>
         <activity android:name="androidx.activity.ViewModelActivity"/>
         <activity android:name="androidx.activity.SavedStateActivity"/>
         <activity android:name="androidx.activity.ContentViewActivity"/>
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
new file mode 100644
index 0000000..70e6672
--- /dev/null
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.activity
+
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.withActivity
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ComponentActivityReportFullyDrawnTest {
+
+    @Test
+    fun testReportFullyDrawn() {
+        with(ActivityScenario.launch(ReportFullyDrawnActivity::class.java)) {
+            withActivity {
+                // This test makes sure that this method does not throw an exception on devices
+                // running API 19 (without UPDATE_DEVICE_STATS permission) and earlier
+                // (regardless or permissions).
+                reportFullyDrawn()
+            }
+        }
+    }
+}
+
+class ReportFullyDrawnActivity : ComponentActivity()
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
index 0b2f103..afb2ff0 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
@@ -310,6 +310,41 @@
     }
 
     @Test
+    fun testUnregisterAfterSavedState() {
+        val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.INITIALIZED)
+        var resultReturned = false
+        val activityResult = registry.register("key", lifecycleOwner, StartActivityForResult()) { }
+
+        activityResult.launch(null)
+
+        val savedState = Bundle()
+        registry.onSaveInstanceState(savedState)
+
+        registry.unregister("key")
+
+        val restoredRegistry = object : ActivityResultRegistry() {
+            override fun <I : Any?, O : Any?> onLaunch(
+                requestCode: Int,
+                contract: ActivityResultContract<I, O>,
+                input: I,
+                options: ActivityOptionsCompat?
+            ) {
+                dispatchResult(requestCode, RESULT_OK, Intent())
+            }
+        }
+
+        restoredRegistry.onRestoreInstanceState(savedState)
+
+        restoredRegistry.register("key", lifecycleOwner, StartActivityForResult()) {
+            resultReturned = true
+        }
+
+        lifecycleOwner.currentState = Lifecycle.State.STARTED
+
+        assertThat(resultReturned).isTrue()
+    }
+
+    @Test
     fun testOnRestoreInstanceState() {
         registry.register("key", StartActivityForResult()) {}
 
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
index 068fe6e..3368985 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -29,6 +29,7 @@
 import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.EXTRA_INTENT_SENDER_REQUEST;
 import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.EXTRA_SEND_INTENT_EXCEPTION;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Context;
@@ -62,6 +63,7 @@
 import androidx.annotation.Nullable;
 import androidx.core.app.ActivityCompat;
 import androidx.core.app.ActivityOptionsCompat;
+import androidx.core.content.ContextCompat;
 import androidx.lifecycle.HasDefaultViewModelProviderFactory;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleEventObserver;
@@ -78,6 +80,7 @@
 import androidx.savedstate.SavedStateRegistryController;
 import androidx.savedstate.SavedStateRegistryOwner;
 import androidx.savedstate.ViewTreeSavedStateRegistryOwner;
+import androidx.tracing.Trace;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -685,4 +688,27 @@
     public final ActivityResultRegistry getActivityResultRegistry() {
         return mActivityResultRegistry;
     }
+
+    @Override
+    public void reportFullyDrawn() {
+        try {
+            if (Trace.isEnabled()) {
+                Trace.beginSection("reportFullyDrawn() for " + getComponentName());
+            }
+
+            if (Build.VERSION.SDK_INT > 19) {
+                super.reportFullyDrawn();
+            } else if (Build.VERSION.SDK_INT == 19 && ContextCompat.checkSelfPermission(this,
+                    Manifest.permission.UPDATE_DEVICE_STATS) == PackageManager.PERMISSION_GRANTED) {
+                // On API 19, the Activity.reportFullyDrawn() method requires the
+                // UPDATE_DEVICE_STATS permission, otherwise it throws an exception. Instead of
+                // throwing, we fall back to a no-op call.
+                super.reportFullyDrawn();
+            }
+            // The Activity.reportFullyDrawn() got added in API 19, fall back to a no-op call if
+            // this method gets called on devices with an earlier version.
+        } finally {
+            Trace.endSection();
+        }
+    }
 }
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
index d491bf6..36d959f 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
@@ -272,7 +272,8 @@
                 new ArrayList<>(mRcToKey.keySet()));
         outState.putStringArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS,
                 new ArrayList<>(mRcToKey.values()));
-        outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS, mPendingResults);
+        outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS,
+                (Bundle) mPendingResults.clone());
         outState.putSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT, mRandom);
     }
 
diff --git a/annotation/annotation-experimental-lint/build.gradle b/annotation/annotation-experimental-lint/build.gradle
index 6c42ad7..bc810c7 100644
--- a/annotation/annotation-experimental-lint/build.gradle
+++ b/annotation/annotation-experimental-lint/build.gradle
@@ -51,9 +51,3 @@
     description = "Lint checks for the Experimental annotation library. Also enforces the " +
             "semantics of Kotlin @Experimental APIs from within Android Java source code."
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += ["-XXLanguage:-NewInference"]
-    }
-}
diff --git a/appcompat/appcompat/api/current.txt b/appcompat/appcompat/api/current.txt
index 5bbbed7..6b91606 100644
--- a/appcompat/appcompat/api/current.txt
+++ b/appcompat/appcompat/api/current.txt
@@ -513,15 +513,14 @@
     method public void setTextAppearance(android.content.Context!, int);
   }
 
-  public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.TintableBackgroundView {
+  public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
     ctor public AppCompatEditText(android.content.Context);
     ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?);
     ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?, int);
-    method public androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>? getRichContentReceiverCompat();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
     method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
-    method public void setRichContentReceiverCompat(androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>?);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
     method public void setTextAppearance(android.content.Context!, int);
diff --git a/appcompat/appcompat/api/public_plus_experimental_current.txt b/appcompat/appcompat/api/public_plus_experimental_current.txt
index cb1efa6..8886644 100644
--- a/appcompat/appcompat/api/public_plus_experimental_current.txt
+++ b/appcompat/appcompat/api/public_plus_experimental_current.txt
@@ -513,15 +513,14 @@
     method public void setTextAppearance(android.content.Context!, int);
   }
 
-  public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.TintableBackgroundView {
+  public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
     ctor public AppCompatEditText(android.content.Context);
     ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?);
     ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?, int);
-    method public androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>? getRichContentReceiverCompat();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
     method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
-    method public void setRichContentReceiverCompat(androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>?);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
     method public void setTextAppearance(android.content.Context!, int);
diff --git a/appcompat/appcompat/api/restricted_current.txt b/appcompat/appcompat/api/restricted_current.txt
index 14d5f94..7439cfd 100644
--- a/appcompat/appcompat/api/restricted_current.txt
+++ b/appcompat/appcompat/api/restricted_current.txt
@@ -1394,15 +1394,14 @@
     method public static void preload();
   }
 
-  public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.TintableBackgroundView {
+  public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
     ctor public AppCompatEditText(android.content.Context);
     ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?);
     ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?, int);
-    method public androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>? getRichContentReceiverCompat();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
     method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
-    method public void setRichContentReceiverCompat(androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>?);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
     method public void setTextAppearance(android.content.Context!, int);
diff --git a/appcompat/appcompat/src/androidTest/AndroidManifest.xml b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
index 9af9433..c216f56 100644
--- a/appcompat/appcompat/src/androidTest/AndroidManifest.xml
+++ b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
@@ -100,8 +100,8 @@
             android:theme="@style/Theme.TextColors"/>
 
         <activity
-            android:name="androidx.appcompat.widget.AppCompatEditTextRichContentReceiverActivity"
-            android:label="@string/app_compat_edit_text_rich_content_receiver_activity"
+            android:name="androidx.appcompat.widget.AppCompatEditTextReceiveContentActivity"
+            android:label="@string/app_compat_edit_text_receive_content_activity"
             android:theme="@style/Theme.AppCompat.Light"/>
 
         <activity
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentActivity.java
similarity index 83%
rename from appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverActivity.java
rename to appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentActivity.java
index 9601ac1..4dc2c31 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverActivity.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentActivity.java
@@ -18,9 +18,9 @@
 import androidx.appcompat.test.R;
 import androidx.appcompat.testutils.BaseTestActivity;
 
-public class AppCompatEditTextRichContentReceiverActivity extends BaseTestActivity {
+public class AppCompatEditTextReceiveContentActivity extends BaseTestActivity {
     @Override
     protected int getContentViewLayoutResId() {
-        return R.layout.appcompat_edittext_richcontentreceiver_activity;
+        return R.layout.appcompat_edittext_receive_content_activity;
     }
 }
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
new file mode 100644
index 0000000..8f46094
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
@@ -0,0 +1,559 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appcompat.widget;
+
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.SpannableStringBuilder;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.test.R;
+import androidx.core.util.ObjectsCompat;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.OnReceiveContentListener;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.inputmethod.EditorInfoCompat;
+import androidx.core.view.inputmethod.InputConnectionCompat;
+import androidx.core.view.inputmethod.InputContentInfoCompat;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AppCompatEditTextReceiveContentTest {
+    private static final String[] MIME_TYPES_IMAGES = new String[] {"image/*"};
+    private static final Uri SAMPLE_CONTENT_URI = Uri.parse("content://com.example/path");
+
+    @Rule
+    public final ActivityTestRule<AppCompatEditTextReceiveContentActivity> mActivityTestRule =
+            new ActivityTestRule<>(AppCompatEditTextReceiveContentActivity.class);
+
+    private Context mContext;
+    private AppCompatEditText mEditText;
+    private OnReceiveContentListener mMockReceiver;
+    private ClipboardManager mClipboardManager;
+
+    @UiThreadTest
+    @Before
+    public void before() {
+        AppCompatActivity activity = mActivityTestRule.getActivity();
+        mContext = activity;
+        mEditText = activity.findViewById(R.id.edit_text);
+
+        mMockReceiver = Mockito.mock(OnReceiveContentListener.class);
+
+        mClipboardManager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+
+        // Clear the clipboard
+        if (Build.VERSION.SDK_INT >= 28) {
+            mClipboardManager.clearPrimaryClip();
+        } else {
+            mClipboardManager.setPrimaryClip(ClipData.newPlainText("", ""));
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    public void testOnCreateInputConnection_nullEditorInfo() throws Exception {
+        setTextAndCursor("xz", 1);
+        try {
+            mEditText.onCreateInputConnection(null);
+            Assert.fail("Expected NullPointerException");
+        } catch (NullPointerException expected) {
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    public void testOnCreateInputConnection_noReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Call onCreateInputConnection() and assert that contentMimeTypes is not set.
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+        assertThat(ic).isNotNull();
+        assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo)).isEqualTo(new String[0]);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testOnCreateInputConnection_withReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a custom impl.
+        String[] mimeTypes = new String[] {"image/*", "video/mp4"};
+        ViewCompat.setOnReceiveContentListener(mEditText, mimeTypes, mMockReceiver);
+
+        // Call onCreateInputConnection() and assert that contentMimeTypes uses the receiver's MIME
+        // types.
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+        assertThat(ic).isNotNull();
+        verifyZeroInteractions(mMockReceiver);
+        assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo)).isEqualTo(mimeTypes);
+    }
+
+    // ============================================================================================
+    // Tests to verify that the listener is invoked for all the appropriate user interactions:
+    // * Paste from clipboard ("Paste" and "Paste as plain text" actions)
+    // * Content insertion from IME
+    // ============================================================================================
+
+    @UiThreadTest
+    @Test
+    public void testPaste_noReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Copy text to the clipboard.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        clip = copyToClipboard(clip);
+
+        // Trigger the "Paste" action. This should execute the platform paste handling, so the
+        // content should be inserted according to whatever behavior is implemented in the OS
+        // version that's running.
+        boolean result = triggerContextMenuAction(android.R.id.paste);
+        assertThat(result).isTrue();
+        if (Build.VERSION.SDK_INT <= 20) {
+            // The platform code on Android K and earlier had logic to insert a space before and
+            // after the pasted content (if no space was already present). See
+            // https://cs.android.com/android/platform/superproject/+/android-4.4.4_r2:frameworks/base/core/java/android/widget/TextView.java;l=8526,8527,8528,8545,8546
+            assertTextAndCursorPosition("x y z", 3);
+        } else {
+            assertTextAndCursorPosition("xyz", 2);
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPaste_withReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Copy text to the clipboard.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        clip = copyToClipboard(clip);
+
+        // Setup: Configure to use the mock receiver.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the "Paste" action and assert that the custom receiver was executed.
+        triggerContextMenuAction(android.R.id.paste);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPaste_withReceiver_resultBoolean() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Copy text to the clipboard.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        clip = copyToClipboard(clip);
+
+        // Setup: Configure to use the mock receiver.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the "Paste" action and assert the boolean it returns.
+        ContentInfoCompat toReturn = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD).build();
+        when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+                .thenReturn(toReturn);
+        boolean result = triggerContextMenuAction(android.R.id.paste);
+        assertThat(result).isTrue();
+
+        when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+                .thenReturn(null);
+        result = triggerContextMenuAction(android.R.id.paste);
+        assertThat(result).isTrue();
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPaste_withReceiver_unsupportedMimeType() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
+        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+                new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
+        clip = copyToClipboard(clip);
+
+        // Setup: Configure to use the mock receiver.
+        String[] mimeTypes = new String[] {"image/*"};
+        ViewCompat.setOnReceiveContentListener(mEditText, mimeTypes, mMockReceiver);
+
+        // Trigger the "Paste" action and assert that the custom receiver was executed. This
+        // confirms that the receiver is invoked (give a chance to handle the content via some
+        // fallback) even if the MIME type of the content is not one of the receiver's supported
+        // MIME types.
+        triggerContextMenuAction(android.R.id.paste);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+    }
+
+    @SdkSuppress(minSdkVersion = 23) // The action "Paste as plain text" was added in SDK 23.
+    @UiThreadTest
+    @Test
+    public void testPasteAsPlainText_noReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Copy HTML to the clipboard.
+        ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
+        clip = copyToClipboard(clip);
+
+        // Trigger the "Paste as plain text" action. This should execute the platform paste
+        // handling, so the content should be inserted according to whatever behavior is implemented
+        // in the OS version that's running.
+        boolean result = triggerContextMenuAction(android.R.id.pasteAsPlainText);
+        assertThat(result).isTrue();
+        assertTextAndCursorPosition("x*y*z", 4);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPasteAsPlainText_withReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Copy text to the clipboard.
+        ClipData clip = ClipData.newPlainText("test", "y");
+        clip = copyToClipboard(clip);
+
+        // Setup: Configure to use the mock receiver.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the "Paste as plain text" action and assert that the custom receiver was
+        // executed.
+        triggerContextMenuAction(android.R.id.pasteAsPlainText);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT));
+        verifyNoMoreInteractions(mMockReceiver);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testPasteAsPlainText_withReceiver_unsupportedMimeType() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
+        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+                new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
+        clip = copyToClipboard(clip);
+
+        // Setup: Configure to use the mock receiver.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the "Paste as plain text" action and assert that the custom receiver was
+        // executed. This confirms that the receiver is invoked (given a chance to handle the
+        // content via some fallback) even if the MIME type of the content is not one of the
+        // receiver's supported MIME types.
+        triggerContextMenuAction(android.R.id.pasteAsPlainText);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT));
+        verifyNoMoreInteractions(mMockReceiver);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_noReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Trigger the IME's commitContent() call and assert its outcome.
+        boolean result = triggerImeCommitContentViaCompat("image/png");
+        assertThat(result).isFalse();
+        assertTextAndCursorPosition("xz", 1);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_withReceiver() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a custom impl.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
+        triggerImeCommitContentViaCompat("image/png");
+        ClipData clip = ClipData.newRawUri("", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText), payloadEq(clip, SOURCE_INPUT_METHOD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_withReceiver_resultBoolean() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a custom impl.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the IME's commitContent() call and assert the boolean value it returns.
+        when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+                .thenReturn(null);
+        boolean result1 = triggerImeCommitContentViaCompat("image/png");
+        ClipData clip = ClipData.newRawUri("", SAMPLE_CONTENT_URI);
+        ContentInfoCompat payloadToReturn =
+                new ContentInfoCompat.Builder(clip, SOURCE_INPUT_METHOD).build();
+        when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+                .thenReturn(payloadToReturn);
+        boolean result2 = triggerImeCommitContentViaCompat("image/png");
+        verify(mMockReceiver, times(2)).onReceiveContent(
+                eq(mEditText), payloadEq(clip, SOURCE_INPUT_METHOD, 0));
+        if (Build.VERSION.SDK_INT >= 25) {
+            // On SDK >= 25, the boolean result depends on the return value from the receiver.
+            assertThat(result1).isTrue();
+            assertThat(result2).isFalse();
+        } else {
+            // On SDK <= 24, commitContent() is handled via InputConnection.performPrivateCommand().
+            // This ends up returning true whenever the command is sent, regardless of the return
+            // value of the underlying operation.
+            // Relevant code links:
+            // https://osscs.corp.google.com/androidx/platform/frameworks/support/+/androidx-master-dev:core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java;l=294;drc=0c365e84832f5ec5e393be28ab1c618eb18bab1e
+            // https://cs.android.com/android/platform/superproject/+/android-7.0.0_r6:frameworks/base/core/java/com/android/internal/widget/EditableInputConnection.java;l=168
+            assertThat(result1).isTrue();
+            assertThat(result2).isTrue();
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_withReceiver_unsupportedMimeType() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a custom impl.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the IME's commitContent() call and assert that the custom receiver was not
+        // executed. This is because InputConnectionCompat.commitContent() checks the supported MIME
+        // types before proceeding.
+        triggerImeCommitContentViaCompat("video/mp4");
+        verifyZeroInteractions(mMockReceiver);
+    }
+
+    @SdkSuppress(minSdkVersion = 25) // InputConnection.commitContent() was added in SDK 25.
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_direct_withReceiver_unsupportedMimeType() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a custom impl.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
+        triggerImeCommitContentDirect("video/mp4");
+        ClipData clip = ClipData.newRawUri("", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText), payloadEq(clip, SOURCE_INPUT_METHOD, 0));
+        verifyNoMoreInteractions(mMockReceiver);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_linkUri() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the IME's commitContent() call with a linkUri and assert receiver extras.
+        Uri sampleLinkUri = Uri.parse("http://example.com");
+        triggerImeCommitContentViaCompat("image/png", sampleLinkUri, null);
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText),
+                payloadEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, null));
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_opts() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the IME's commitContent() call with opts and assert receiver extras.
+        String sampleOptValue = "sampleOptValue";
+        triggerImeCommitContentViaCompat("image/png", null, sampleOptValue);
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText),
+                payloadEq(clip, SOURCE_INPUT_METHOD, 0, null, sampleOptValue));
+    }
+
+    @UiThreadTest
+    @Test
+    public void testImeCommitContent_linkUriAndOpts() throws Exception {
+        setTextAndCursor("xz", 1);
+
+        // Setup: Configure the receiver to a mock impl.
+        ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+        // Trigger the IME's commitContent() call with a linkUri & opts and assert receiver extras.
+        Uri sampleLinkUri = Uri.parse("http://example.com");
+        String sampleOptValue = "sampleOptValue";
+        triggerImeCommitContentViaCompat("image/png", sampleLinkUri, sampleOptValue);
+        ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+        verify(mMockReceiver, times(1)).onReceiveContent(
+                eq(mEditText),
+                payloadEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, sampleOptValue));
+    }
+
+    private boolean triggerContextMenuAction(final int actionId) {
+        return mEditText.onTextContextMenuItem(actionId);
+    }
+
+    private boolean triggerImeCommitContentViaCompat(String mimeType) {
+        return triggerImeCommitContentViaCompat(mimeType, null, null);
+    }
+
+    private boolean triggerImeCommitContentViaCompat(String mimeType, Uri linkUri, String extra) {
+        final InputContentInfoCompat contentInfo = new InputContentInfoCompat(
+                SAMPLE_CONTENT_URI,
+                new ClipDescription("from test", new String[]{mimeType}),
+                linkUri);
+        final Bundle opts;
+        if (extra == null) {
+            opts = null;
+        } else {
+            opts = new Bundle();
+            opts.putString(PayloadArgumentMatcher.EXTRA_KEY, extra);
+        }
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+        return InputConnectionCompat.commitContent(ic, editorInfo, contentInfo, 0, opts);
+    }
+
+    private boolean triggerImeCommitContentDirect(String mimeType) {
+        final InputContentInfo contentInfo = new InputContentInfo(
+                SAMPLE_CONTENT_URI,
+                new ClipDescription("from test", new String[]{mimeType}),
+                null);
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+        return ic.commitContent(contentInfo, 0, null);
+    }
+
+    private void setTextAndCursor(final String text, final int cursorPosition) {
+        mEditText.requestFocus();
+        SpannableStringBuilder ssb = new SpannableStringBuilder(text);
+        mEditText.setText(ssb);
+        mEditText.setSelection(cursorPosition);
+        assertThat(mEditText.hasFocus()).isTrue();
+        assertTextAndCursorPosition(text, cursorPosition);
+    }
+
+    private void assertTextAndCursorPosition(String expectedText, int cursorPosition) {
+        assertThat(mEditText.getText().toString()).isEqualTo(expectedText);
+        assertThat(mEditText.getSelectionStart()).isEqualTo(cursorPosition);
+        assertThat(mEditText.getSelectionEnd()).isEqualTo(cursorPosition);
+    }
+
+    private ClipData copyToClipboard(final ClipData clip) {
+        mClipboardManager.setPrimaryClip(clip);
+        ClipData primaryClip = mClipboardManager.getPrimaryClip();
+        assertThat(primaryClip).isNotNull();
+        return primaryClip;
+    }
+
+    private static ContentInfoCompat payloadEq(@NonNull ClipData clip, int source, int flags) {
+        return argThat(new PayloadArgumentMatcher(clip, source, flags, null, null));
+    }
+
+    private static ContentInfoCompat payloadEq(@NonNull ClipData clip, int source, int flags,
+            @Nullable Uri linkUri, @Nullable String extra) {
+        return argThat(new PayloadArgumentMatcher(clip, source, flags, linkUri, extra));
+    }
+
+    private static class PayloadArgumentMatcher implements ArgumentMatcher<ContentInfoCompat> {
+        public static final String EXTRA_KEY = "testExtra";
+
+        @NonNull
+        private final ClipData mClip;
+        private final int mSource;
+        private final int mFlags;
+        @Nullable
+        private final Uri mLinkUri;
+        @Nullable
+        private final String mExtra;
+
+        private PayloadArgumentMatcher(@NonNull ClipData clip, int source, int flags,
+                @Nullable Uri linkUri, @Nullable String extra) {
+            mClip = clip;
+            mSource = source;
+            mFlags = flags;
+            mLinkUri = linkUri;
+            mExtra = extra;
+        }
+
+        @Override
+        public boolean matches(ContentInfoCompat actual) {
+            ClipData.Item expectedItem = mClip.getItemAt(0);
+            ClipData.Item actualItem = actual.getClip().getItemAt(0);
+            return ObjectsCompat.equals(expectedItem.getText(), actualItem.getText())
+                    && ObjectsCompat.equals(expectedItem.getUri(), actualItem.getUri())
+                    && mSource == actual.getSource()
+                    && mFlags == actual.getFlags()
+                    && ObjectsCompat.equals(mLinkUri, actual.getLinkUri())
+                    && extrasMatch(actual.getExtras());
+        }
+
+        private boolean extrasMatch(Bundle actualExtras) {
+            if (mExtra == null) {
+                return actualExtras == null;
+            }
+            String actualExtraValue = actualExtras.getString(EXTRA_KEY);
+            return ObjectsCompat.equals(mExtra, actualExtraValue);
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverTest.java
deleted file mode 100644
index 5f68405..0000000
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverTest.java
+++ /dev/null
@@ -1,489 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appcompat.widget;
-
-import static androidx.core.widget.RichContentReceiverCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_CLIPBOARD;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_INPUT_METHOD;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.net.Uri;
-import android.os.Build;
-import android.text.SpannableStringBuilder;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputContentInfo;
-import android.widget.TextView;
-
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.test.R;
-import androidx.core.view.inputmethod.EditorInfoCompat;
-import androidx.core.view.inputmethod.InputConnectionCompat;
-import androidx.core.view.inputmethod.InputContentInfoCompat;
-import androidx.core.widget.RichContentReceiverCompat;
-import androidx.core.widget.TextViewRichContentReceiverCompat;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.rule.ActivityTestRule;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mockito;
-
-import java.util.Set;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class AppCompatEditTextRichContentReceiverTest {
-    private static final Set<String> ALL_TEXT_AND_IMAGE_MIME_TYPES = ImmutableSet.of(
-            "text/*", "image/*");
-
-    @Rule
-    public final ActivityTestRule<AppCompatEditTextRichContentReceiverActivity> mActivityTestRule =
-            new ActivityTestRule<>(AppCompatEditTextRichContentReceiverActivity.class);
-
-    private Context mContext;
-    private AppCompatEditText mEditText;
-    private RichContentReceiverCompat<TextView> mMockReceiver;
-    private ClipboardManager mClipboardManager;
-
-    @UiThreadTest
-    @Before
-    public void before() {
-        AppCompatActivity activity = mActivityTestRule.getActivity();
-        mContext = activity;
-        mEditText = activity.findViewById(R.id.edit_text_default_values);
-
-        mMockReceiver = Mockito.mock(RichContentReceiverCompat.class);
-
-        mClipboardManager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
-
-        // Clear the clipboard
-        if (Build.VERSION.SDK_INT >= 28) {
-            mClipboardManager.clearPrimaryClip();
-        } else {
-            mClipboardManager.setPrimaryClip(ClipData.newPlainText("", ""));
-        }
-    }
-
-    // ============================================================================================
-    // Tests to verify APIs/accessors/defaults related to RichContentReceiver.
-    // ============================================================================================
-
-    @UiThreadTest
-    @Test
-    public void testGetAndSetRichContentReceiverCompat() throws Exception {
-        // Verify that by default the getter returns null.
-        assertThat(mEditText.getRichContentReceiverCompat()).isNull();
-
-        // Verify that after setting a custom receiver, the getter returns it.
-        TextViewRichContentReceiverCompat receiver = new TextViewRichContentReceiverCompat() {};
-        mEditText.setRichContentReceiverCompat(receiver);
-        assertThat(mEditText.getRichContentReceiverCompat()).isSameInstanceAs(receiver);
-
-        // Verify that the receiver can be reset by passing null.
-        mEditText.setRichContentReceiverCompat(null);
-        assertThat(mEditText.getRichContentReceiverCompat()).isNull();
-    }
-
-    @UiThreadTest
-    @Test
-    public void testOnCreateInputConnection_nullEditorInfo() throws Exception {
-        setTextAndCursor("xz", 1);
-        try {
-            mEditText.onCreateInputConnection(null);
-            Assert.fail("Expected NullPointerException");
-        } catch (NullPointerException expected) {
-        }
-    }
-
-    @UiThreadTest
-    @Test
-    public void testOnCreateInputConnection_noReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Call onCreateInputConnection() and assert that contentMimeTypes is not set.
-        EditorInfo editorInfo = new EditorInfo();
-        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
-        assertThat(ic).isNotNull();
-        assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo)).isEqualTo(new String[0]);
-    }
-
-    @UiThreadTest
-    @Test
-    public void testOnCreateInputConnection_withReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Configure the receiver to a custom impl.
-        Set<String> receiverMimeTypes = ImmutableSet.of("text/plain", "image/png", "video/mp4");
-        when(mMockReceiver.getSupportedMimeTypes()).thenReturn(receiverMimeTypes);
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Call onCreateInputConnection() and assert that contentMimeTypes is set from the receiver.
-        EditorInfo editorInfo = new EditorInfo();
-        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
-        assertThat(ic).isNotNull();
-        verify(mMockReceiver, times(1)).getSupportedMimeTypes();
-        verifyNoMoreInteractions(mMockReceiver);
-        assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo))
-                .isEqualTo(receiverMimeTypes.toArray(new String[0]));
-    }
-
-    // ============================================================================================
-    // Tests to verify that the receiver callback is invoked for all the appropriate user
-    // interactions:
-    // * Paste from clipboard ("Paste" and "Paste as plain text" actions)
-    // * Content insertion from IME
-    // ============================================================================================
-
-    @UiThreadTest
-    @Test
-    public void testPaste_noReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Copy text to the clipboard.
-        ClipData clip = ClipData.newPlainText("test", "y");
-        clip = copyToClipboard(clip);
-
-        // Trigger the "Paste" action. This should execute the platform paste handling, so the
-        // content should be inserted according to whatever behavior is implemented in the OS
-        // version that's running.
-        boolean result = triggerContextMenuAction(android.R.id.paste);
-        assertThat(result).isTrue();
-        if (Build.VERSION.SDK_INT <= 20) {
-            // The platform code on Android K and earlier had logic to insert a space before and
-            // after the pasted content (if no space was already present). See
-            // https://cs.android.com/android/platform/superproject/+/android-4.4.4_r2:frameworks/base/core/java/android/widget/TextView.java;l=8526,8527,8528,8545,8546
-            assertTextAndCursorPosition("x y z", 3);
-        } else {
-            assertTextAndCursorPosition("xyz", 2);
-        }
-    }
-
-    @UiThreadTest
-    @Test
-    public void testPaste_withReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Copy text to the clipboard.
-        ClipData clip = ClipData.newPlainText("test", "y");
-        clip = copyToClipboard(clip);
-
-        // Setup: Configure to use the mock receiver.
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the "Paste" action and assert that the custom receiver was executed.
-        triggerContextMenuAction(android.R.id.paste);
-        verify(mMockReceiver, times(1)).onReceive(
-                eq(mEditText), clipEq(clip), eq(SOURCE_CLIPBOARD), eq(0));
-        verifyNoMoreInteractions(mMockReceiver);
-    }
-
-    @UiThreadTest
-    @Test
-    public void testPaste_withReceiver_resultBoolean() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Copy text to the clipboard.
-        ClipData clip = ClipData.newPlainText("test", "y");
-        clip = copyToClipboard(clip);
-
-        // Setup: Configure to use the mock receiver.
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the "Paste" action and assert that the boolean result is true regardless of
-        // the receiver's return value.
-        when(mMockReceiver.onReceive(eq(mEditText), eq(clip), eq(SOURCE_CLIPBOARD),
-                eq(FLAG_CONVERT_TO_PLAIN_TEXT))).thenReturn(true);
-        boolean result = triggerContextMenuAction(android.R.id.paste);
-        assertThat(result).isTrue();
-
-        when(mMockReceiver.onReceive(eq(mEditText), eq(clip), eq(SOURCE_CLIPBOARD),
-                eq(FLAG_CONVERT_TO_PLAIN_TEXT))).thenReturn(false);
-        result = triggerContextMenuAction(android.R.id.paste);
-        assertThat(result).isTrue();
-    }
-
-    @UiThreadTest
-    @Test
-    public void testPaste_withReceiver_unsupportedMimeType() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
-        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
-                new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
-        clip = copyToClipboard(clip);
-
-        // Setup: Configure to use the mock receiver.
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the "Paste" action and assert that the custom receiver was executed. This
-        // confirms that the receiver is invoked (give a chance to handle the content via some
-        // fallback) even if the MIME type of the content is not one of the receiver's supported
-        // MIME types.
-        triggerContextMenuAction(android.R.id.paste);
-        verify(mMockReceiver, times(1)).onReceive(
-                eq(mEditText), clipEq(clip), eq(SOURCE_CLIPBOARD), eq(0));
-        verifyNoMoreInteractions(mMockReceiver);
-    }
-
-    @SdkSuppress(minSdkVersion = 23) // The action "Paste as plain text" was added in SDK 23.
-    @UiThreadTest
-    @Test
-    public void testPasteAsPlainText_noReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Copy HTML to the clipboard.
-        ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
-        clip = copyToClipboard(clip);
-
-        // Trigger the "Paste as plain text" action. This should execute the platform paste
-        // handling, so the content should be inserted according to whatever behavior is implemented
-        // in the OS version that's running.
-        boolean result = triggerContextMenuAction(android.R.id.pasteAsPlainText);
-        assertThat(result).isTrue();
-        assertTextAndCursorPosition("x*y*z", 4);
-    }
-
-    @UiThreadTest
-    @Test
-    public void testPasteAsPlainText_withReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Copy text to the clipboard.
-        ClipData clip = ClipData.newPlainText("test", "y");
-        clip = copyToClipboard(clip);
-
-        // Setup: Configure to use the mock receiver.
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the "Paste as plain text" action and assert that the custom receiver was
-        // executed.
-        triggerContextMenuAction(android.R.id.pasteAsPlainText);
-        verify(mMockReceiver, times(1)).onReceive(
-                eq(mEditText), clipEq(clip),
-                eq(SOURCE_CLIPBOARD), eq(FLAG_CONVERT_TO_PLAIN_TEXT));
-        verifyNoMoreInteractions(mMockReceiver);
-    }
-
-    @UiThreadTest
-    @Test
-    public void testPasteAsPlainText_withReceiver_unsupportedMimeType() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
-        ClipData clip = new ClipData("test", new String[]{"video/mp4"},
-                new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
-        clip = copyToClipboard(clip);
-
-        // Setup: Configure to use the mock receiver.
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the "Paste as plain text" action and assert that the custom receiver was
-        // executed. This confirms that the receiver is invoked (given a chance to handle the
-        // content via some fallback) even if the MIME type of the content is not one of the
-        // receiver's supported MIME types.
-        triggerContextMenuAction(android.R.id.pasteAsPlainText);
-        verify(mMockReceiver, times(1)).onReceive(
-                eq(mEditText), clipEq(clip),
-                eq(SOURCE_CLIPBOARD), eq(FLAG_CONVERT_TO_PLAIN_TEXT));
-        verifyNoMoreInteractions(mMockReceiver);
-    }
-
-    @UiThreadTest
-    @Test
-    public void testImeCommitContent_noReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Trigger the IME's commitContent() call and assert its outcome.
-        boolean result = triggerImeCommitContentViaCompat("image/png");
-        assertThat(result).isFalse();
-        assertTextAndCursorPosition("xz", 1);
-    }
-
-    @UiThreadTest
-    @Test
-    public void testImeCommitContent_withReceiver() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Configure the receiver to a custom impl that supports all text and images.
-        when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
-        triggerImeCommitContentViaCompat("image/png");
-        verify(mMockReceiver, times(1)).getSupportedMimeTypes();
-        verify(mMockReceiver, times(1)).onReceive(
-                eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD), eq(0));
-        verifyNoMoreInteractions(mMockReceiver);
-    }
-
-    @UiThreadTest
-    @Test
-    public void testImeCommitContent_withReceiver_resultBoolean() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Configure the receiver to a custom impl that supports all text and images.
-        when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the IME's commitContent() call, once when the mock receiver is configured to
-        // return true and once when the mock receiver is configured to return false.
-        when(mMockReceiver.onReceive(eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD),
-                eq(0))).thenReturn(true);
-        boolean result1 = triggerImeCommitContentViaCompat("image/png");
-        when(mMockReceiver.onReceive(eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD),
-                eq(0))).thenReturn(false);
-        boolean result2 = triggerImeCommitContentViaCompat("image/png");
-        verify(mMockReceiver, times(2)).onReceive(
-                eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD), eq(0));
-        if (Build.VERSION.SDK_INT >= 25) {
-            // On SDK 25 and above, the boolean result should match the return value from the
-            // receiver.
-            assertThat(result1).isTrue();
-            assertThat(result2).isFalse();
-        } else {
-            // On SDK 24 and below, commitContent() is handled via
-            // InputConnection.performPrivateCommand(). This ends up returning true whenever the
-            // command is sent, regardless of the return value of the underlying operation.
-            // Relevant code links:
-            // https://osscs.corp.google.com/androidx/platform/frameworks/support/+/androidx-master-dev:core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java;l=294;drc=0c365e84832f5ec5e393be28ab1c618eb18bab1e
-            // https://cs.android.com/android/platform/superproject/+/android-7.0.0_r6:frameworks/base/core/java/com/android/internal/widget/EditableInputConnection.java;l=168
-            assertThat(result1).isTrue();
-            assertThat(result2).isTrue();
-        }
-    }
-
-    @UiThreadTest
-    @Test
-    public void testImeCommitContent_withReceiver_unsupportedMimeType() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Configure the receiver to a custom impl that supports all text and images.
-        when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the IME's commitContent() call and assert that the custom receiver was not
-        // executed. This is because InputConnectionCompat.commitContent() checks the supported MIME
-        // types before proceeding.
-        triggerImeCommitContentViaCompat("video/mp4");
-        verify(mMockReceiver, times(1)).getSupportedMimeTypes();
-        verifyNoMoreInteractions(mMockReceiver);
-    }
-
-    @SdkSuppress(minSdkVersion = 25) // InputConnection.commitContent() was added in SDK 25.
-    @UiThreadTest
-    @Test
-    public void testImeCommitContent_direct_withReceiver_unsupportedMimeType() throws Exception {
-        setTextAndCursor("xz", 1);
-
-        // Setup: Configure the receiver to a custom impl that supports all text and images.
-        when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
-        mEditText.setRichContentReceiverCompat(mMockReceiver);
-
-        // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
-        triggerImeCommitContentDirect("video/mp4");
-        verify(mMockReceiver, times(1)).getSupportedMimeTypes();
-        verify(mMockReceiver, times(1)).onReceive(
-                eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD), eq(0));
-        verifyNoMoreInteractions(mMockReceiver);
-    }
-
-    private boolean triggerContextMenuAction(final int actionId) {
-        return mEditText.onTextContextMenuItem(actionId);
-    }
-
-    private boolean triggerImeCommitContentViaCompat(String mimeType) {
-        final InputContentInfoCompat contentInfo = new InputContentInfoCompat(
-                Uri.parse("content://com.example/path"),
-                new ClipDescription("from test", new String[]{mimeType}),
-                Uri.parse("https://example.com"));
-        EditorInfo editorInfo = new EditorInfo();
-        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
-        return InputConnectionCompat.commitContent(ic, editorInfo, contentInfo, 0, null);
-    }
-
-    private boolean triggerImeCommitContentDirect(String mimeType) {
-        final InputContentInfo contentInfo = new InputContentInfo(
-                Uri.parse("content://com.example/path"),
-                new ClipDescription("from test", new String[]{mimeType}),
-                Uri.parse("https://example.com"));
-        EditorInfo editorInfo = new EditorInfo();
-        InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
-        return ic.commitContent(contentInfo, 0, null);
-    }
-
-    private void setTextAndCursor(final String text, final int cursorPosition) {
-        mEditText.requestFocus();
-        SpannableStringBuilder ssb = new SpannableStringBuilder(text);
-        mEditText.setText(ssb);
-        mEditText.setSelection(cursorPosition);
-        assertThat(mEditText.hasFocus()).isTrue();
-        assertTextAndCursorPosition(text, cursorPosition);
-    }
-
-    private void assertTextAndCursorPosition(String expectedText, int cursorPosition) {
-        assertThat(mEditText.getText().toString()).isEqualTo(expectedText);
-        assertThat(mEditText.getSelectionStart()).isEqualTo(cursorPosition);
-        assertThat(mEditText.getSelectionEnd()).isEqualTo(cursorPosition);
-    }
-
-    private ClipData copyToClipboard(final ClipData clip) {
-        mClipboardManager.setPrimaryClip(clip);
-        ClipData primaryClip = mClipboardManager.getPrimaryClip();
-        assertThat(primaryClip).isNotNull();
-        return primaryClip;
-    }
-
-    private static ClipData clipEq(ClipData expected) {
-        return argThat(new ClipDataArgumentMatcher(expected));
-    }
-
-    private static class ClipDataArgumentMatcher implements ArgumentMatcher<ClipData> {
-        private final ClipData mExpected;
-
-        private ClipDataArgumentMatcher(ClipData expected) {
-            this.mExpected = expected;
-        }
-
-        @Override
-        public boolean matches(ClipData actual) {
-            return mExpected.getItemAt(0).getText().equals(actual.getItemAt(0).getText());
-        }
-    }
-}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java
index 63d04a8..92aba86 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java
@@ -24,8 +24,10 @@
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import android.graphics.Typeface;
+import android.os.Build;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityNodeInfo;
 
@@ -87,21 +89,37 @@
         AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
         switchButton.onInitializeAccessibilityNodeInfo(info);
         assertEquals("android.widget.Switch", info.getClassName());
-        assertEquals(mActivity.getResources().getString(R.string.sample_text1), info.getText());
-        assertEquals(
-                mActivity.getResources().getString(androidx.appcompat.R.string.abc_capital_off),
-                ViewCompat.getStateDescription(switchButton)
-        );
+        final String capitalOff =
+                mActivity.getResources().getString(androidx.appcompat.R.string.abc_capital_off);
+        final String text = mActivity.getResources().getString(R.string.sample_text1);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            assertEquals(text + " " + capitalOff, info.getText());
+            assertNull(ViewCompat.getStateDescription(switchButton));
+        } else {
+            assertEquals(text, info.getText());
+            assertEquals(capitalOff, ViewCompat.getStateDescription(switchButton));
+        }
+        info.recycle();
     }
 
     @Test
     public void testAccessibility_textOnOff() {
         final SwitchCompat switchButton = mContainer.findViewById(R.id.switch_textOnOff);
+        final CharSequence textOn = "testStateOn";
+        final CharSequence textOff = "testStateOff";
         AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
         switchButton.onInitializeAccessibilityNodeInfo(info);
         assertEquals("android.widget.Switch", info.getClassName());
-        assertEquals(mActivity.getResources().getString(R.string.sample_text1), info.getText());
-        assertEquals("testStateOff", ViewCompat.getStateDescription(switchButton));
+        final String text = mActivity.getResources().getString(R.string.sample_text1);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            assertEquals(text + " " + textOff, info.getText());
+            assertNull(ViewCompat.getStateDescription(switchButton));
+        } else {
+            assertEquals(text, info.getText());
+            assertEquals(textOff, ViewCompat.getStateDescription(switchButton));
+        }
+        info.recycle();
+
         final CharSequence newTextOff = "new text off";
         final CharSequence newTextOn = "new text on";
         mActivity.runOnUiThread(
@@ -109,12 +127,41 @@
                     @Override
                     public void run() {
                         switchButton.toggle();
-                        assertEquals("testStateOn", ViewCompat.getStateDescription(switchButton));
-                        switchButton.setTextOff(newTextOff);
+                        AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+                        switchButton.onInitializeAccessibilityNodeInfo(info);
+                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+                            assertEquals(text + " " + textOn, info.getText());
+                            assertNull(ViewCompat.getStateDescription(switchButton));
+                        } else {
+                            assertEquals(text, info.getText());
+                            assertEquals(textOn, ViewCompat.getStateDescription(switchButton));
+                        }
+                        info.recycle();
+
                         switchButton.setTextOn(newTextOn);
-                        assertEquals(newTextOn, ViewCompat.getStateDescription(switchButton));
+                        switchButton.setTextOff(newTextOff);
+                        info = AccessibilityNodeInfo.obtain();
+                        switchButton.onInitializeAccessibilityNodeInfo(info);
+                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+                            assertEquals(text + " " + newTextOn, info.getText());
+                            assertNull(ViewCompat.getStateDescription(switchButton));
+                        } else {
+                            assertEquals(text, info.getText());
+                            assertEquals(newTextOn, ViewCompat.getStateDescription(switchButton));
+                        }
+                        info.recycle();
+
                         switchButton.toggle();
-                        assertEquals(newTextOff, ViewCompat.getStateDescription(switchButton));
+                        info = AccessibilityNodeInfo.obtain();
+                        switchButton.onInitializeAccessibilityNodeInfo(info);
+                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+                            assertEquals(text + " " + newTextOff, info.getText());
+                            assertNull(ViewCompat.getStateDescription(switchButton));
+                        } else {
+                            assertEquals(text, info.getText());
+                            assertEquals(newTextOff, ViewCompat.getStateDescription(switchButton));
+                        }
+                        info.recycle();
                     }
                 }
         );
diff --git a/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_richcontentreceiver_activity.xml b/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_receive_content_activity.xml
similarity index 95%
rename from appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_richcontentreceiver_activity.xml
rename to appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_receive_content_activity.xml
index b1c4421..c1706b6 100644
--- a/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_richcontentreceiver_activity.xml
+++ b/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_receive_content_activity.xml
@@ -28,7 +28,7 @@
         android:orientation="vertical">
 
         <androidx.appcompat.widget.AppCompatEditText
-            android:id="@+id/edit_text_default_values"
+            android:id="@+id/edit_text"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
     </LinearLayout>
diff --git a/appcompat/appcompat/src/androidTest/res/values/strings.xml b/appcompat/appcompat/src/androidTest/res/values/strings.xml
index 19145b8..563bc0e 100644
--- a/appcompat/appcompat/src/androidTest/res/values/strings.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/strings.xml
@@ -57,7 +57,9 @@
     <string name="app_compat_text_view_activity">AppCompat text view</string>
     <string name="app_compat_text_view_auto_size_activity">AppCompat text view auto-size</string>
     <string name="app_compat_edit_text_activity">AppCompat edit text</string>
-    <string name="app_compat_edit_text_rich_content_receiver_activity">AppCompat edit text rich content receiver</string>
+    <string name="app_compat_edit_text_receive_content_activity">
+        AppCompat edit text receive content activity
+    </string>
     <string name="app_compat_button_auto_size_activity">AppCompat button auto-size</string>
     <string name="sample_text1">Sample text 1</string>
     <string name="sample_text2">Sample text 2</string>
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
index 30e4e6f..704589c 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
@@ -17,8 +17,10 @@
 package androidx.appcompat.widget;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
-import static androidx.core.widget.RichContentReceiverCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.Builder;
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
 
 import android.content.ClipData;
 import android.content.ClipboardManager;
@@ -27,9 +29,12 @@
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.os.Bundle;
 import android.text.Editable;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.ActionMode;
+import android.view.View;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.view.textclassifier.TextClassifier;
@@ -42,10 +47,17 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.appcompat.R;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.OnReceiveContentListener;
+import androidx.core.view.OnReceiveContentViewBehavior;
 import androidx.core.view.TintableBackgroundView;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.inputmethod.EditorInfoCompat;
 import androidx.core.view.inputmethod.InputConnectionCompat;
-import androidx.core.widget.RichContentReceiverCompat;
+import androidx.core.view.inputmethod.InputConnectionCompat.OnCommitContentListener;
+import androidx.core.view.inputmethod.InputContentInfoCompat;
 import androidx.core.widget.TextViewCompat;
+import androidx.core.widget.TextViewOnReceiveContentListener;
 
 /**
  * A {@link EditText} which supports compatible features on older versions of the platform,
@@ -55,8 +67,8 @@
  *     {@link androidx.core.view.ViewCompat}.</li>
  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
  *     {@link R.attr#backgroundTintMode}.</li>
- *     <li>Allows setting a custom {@link RichContentReceiverCompat receiver callback} in order to
- *     handle insertion of content (e.g. pasting text or an image from the clipboard). This callback
+ *     <li>Allows setting a custom {@link OnReceiveContentListener listener} to handle
+ *     insertion of content (e.g. pasting text or an image from the clipboard). This listener
  *     provides the opportunity to implement app-specific handling such as creating an attachment
  *     when an image is pasted.</li>
  * </ul>
@@ -66,13 +78,14 @@
  * <a href="{@docRoot}topic/libraries/support-library/packages.html#v7-appcompat">appcompat</a>.
  * You should only need to manually use this class when writing custom views.</p>
  */
-public class AppCompatEditText extends EditText implements TintableBackgroundView {
+public class AppCompatEditText extends EditText implements TintableBackgroundView,
+        OnReceiveContentViewBehavior {
+    private static final String LOG_TAG = "AppCompatEditText";
 
     private final AppCompatBackgroundHelper mBackgroundTintHelper;
     private final AppCompatTextHelper mTextHelper;
     private final AppCompatTextClassifierHelper mTextClassifierHelper;
-    @Nullable
-    private RichContentReceiverCompat<TextView> mRichContentReceiverCompat;
+    private final TextViewOnReceiveContentListener mDefaultOnReceiveContentListener;
 
     public AppCompatEditText(@NonNull Context context) {
         this(context, null);
@@ -96,6 +109,8 @@
         mTextHelper.applyCompoundDrawablesTints();
 
         mTextClassifierHelper = new AppCompatTextClassifierHelper(this);
+
+        mDefaultOnReceiveContentListener = new TextViewOnReceiveContentListener();
     }
 
     /**
@@ -204,26 +219,62 @@
     }
 
     /**
-     * If a {@link #setRichContentReceiverCompat receiver callback} is set, the returned
+     * If a {@link ViewCompat#setOnReceiveContentListener listener is set}, the returned
      * {@link InputConnection} will use it to handle calls to {@link InputConnection#commitContent}.
      *
      * {@inheritDoc}
      */
+    @Nullable
     @Override
     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
         InputConnection ic = super.onCreateInputConnection(outAttrs);
         mTextHelper.populateSurroundingTextIfNeeded(this, ic, outAttrs);
         ic = AppCompatHintHelper.onCreateInputConnection(ic, outAttrs, this);
-        if (ic != null && mRichContentReceiverCompat != null) {
-            mRichContentReceiverCompat.populateEditorInfoContentMimeTypes(ic, outAttrs);
-            InputConnectionCompat.OnCommitContentListener callback =
-                    mRichContentReceiverCompat.buildOnCommitContentListener(this);
-            ic = InputConnectionCompat.createWrapper(ic, outAttrs, callback);
+
+        String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
+        if (ic != null && mimeTypes != null) {
+            EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
+            OnCommitContentListener onCommitContentListener = buildOnCommitContentListener(this);
+            ic = InputConnectionCompat.createWrapper(ic, outAttrs, onCommitContentListener);
         }
         return ic;
     }
 
     /**
+     * Creates an {@link InputConnectionCompat.OnCommitContentListener} that uses
+     * {@link ViewCompat#performReceiveContent} to insert content. The listener returned by this
+     * function should be passed to {@link InputConnectionCompat#createWrapper} when creating the
+     * {@link InputConnection} in {@link View#onCreateInputConnection}.
+     */
+    // TODO(b/150318135): Generalize/extract this so it can be reused for other widgets
+    @NonNull
+    private static InputConnectionCompat.OnCommitContentListener buildOnCommitContentListener(
+            @NonNull final View view) {
+        return new InputConnectionCompat.OnCommitContentListener() {
+            @Override
+            public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
+                    Bundle opts) {
+                if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+                    try {
+                        inputContentInfo.requestPermission();
+                    } catch (Exception e) {
+                        Log.w(LOG_TAG,
+                                "Can't insert content from IME; requestPermission() failed", e);
+                        return false;
+                    }
+                }
+                ClipData clip = new ClipData(inputContentInfo.getDescription(),
+                        new ClipData.Item(inputContentInfo.getContentUri()));
+                ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_INPUT_METHOD)
+                        .setLinkUri(inputContentInfo.getLinkUri())
+                        .setExtras(opts)
+                        .build();
+                return ViewCompat.performReceiveContent(view, payload) == null;
+            }
+        };
+    }
+
+    /**
      * See
      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
      */
@@ -264,23 +315,23 @@
     }
 
     /**
-     * If a {@link #setRichContentReceiverCompat receiver callback} is set, uses it to execute the
+     * If a {@link ViewCompat#setOnReceiveContentListener listener is set}, uses it to execute the
      * "Paste" and "Paste as plain text" menu actions.
      *
      * {@inheritDoc}
      */
     @Override
     public boolean onTextContextMenuItem(int id) {
-        if (mRichContentReceiverCompat == null) {
-            return super.onTextContextMenuItem(id);
-        }
-        if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) {
+        if (ViewCompat.getOnReceiveContentMimeTypes(this) != null
+                && (id == android.R.id.paste || id == android.R.id.pasteAsPlainText)) {
             ClipboardManager cm = (ClipboardManager) getContext().getSystemService(
                     Context.CLIPBOARD_SERVICE);
-            ClipData clip = cm == null ? null : cm.getPrimaryClip();
+            ClipData clip = (cm == null) ? null : cm.getPrimaryClip();
             if (clip != null) {
-                int flags = (id == android.R.id.paste) ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT;
-                mRichContentReceiverCompat.onReceive(this, clip, SOURCE_CLIPBOARD, flags);
+                ContentInfoCompat payload =  new Builder(clip, SOURCE_CLIPBOARD)
+                        .setFlags((id == android.R.id.paste) ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT)
+                        .build();
+                ViewCompat.performReceiveContent(this, payload);
             }
             return true;
         }
@@ -288,39 +339,23 @@
     }
 
     /**
-     * Returns the callback that handles insertion of content into this view (e.g. pasting from
-     * the clipboard). See {@link #setRichContentReceiverCompat} for more info.
+     * Implements the default behavior for receiving content, which coerces all content to text
+     * and inserts into the view.
      *
-     * @return The callback that this view is using to handle insertion of content. Returns
-     * {@code null} if no callback is configured, in which case the platform behavior of the
-     * {@link EditText} component will be used for content insertion.
+     * <p>Subclasses of this widget can override this method to customize the default behavior
+     * for receiving content. Apps wishing to provide custom behavior for receiving content
+     * should set a listener via {@link ViewCompat#setOnReceiveContentListener}.
+     *
+     * <p>See {@link ViewCompat#performReceiveContent} for more info.
+     *
+     * @param payload The content to insert and related metadata.
+     *
+     * @return The portion of the passed-in content that was not handled (may be all, some, or none
+     * of the passed-in content).
      */
     @Nullable
-    public RichContentReceiverCompat<TextView> getRichContentReceiverCompat() {
-        return mRichContentReceiverCompat;
-    }
-
-    /**
-     * Sets the callback to handle insertion of content into this view.
-     *
-     * <p>"Content" and "rich content" here refers to both text and non-text: plain text, styled
-     * text, HTML, images, videos, audio files, etc. The callback configured here should typically
-     * extend from {@link androidx.core.widget.TextViewRichContentReceiverCompat} to provide
-     * consistent behavior for text content.
-     *
-     * <p>This callback will be invoked for the following scenarios:
-     * <ol>
-     *     <li>Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the
-     *     insertion/selection menu)
-     *     <li>Content insertion from the keyboard ({@link InputConnection#commitContent})
-     * </ol>
-     *
-     * @param receiver The callback to use. This can be {@code null} to clear any previously set
-     *                 callback (the platform behavior of the {@link EditText} component will then
-     *                 be used).
-     */
-    public void setRichContentReceiverCompat(
-            @Nullable RichContentReceiverCompat<TextView> receiver) {
-        mRichContentReceiverCompat = receiver;
+    @Override
+    public ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload) {
+        return mDefaultOnReceiveContentListener.onReceiveContent(this, payload);
     }
 }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
index 5e25a81..f4d72a52 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
@@ -775,9 +775,11 @@
     public void setTextOn(CharSequence textOn) {
         mTextOn = textOn;
         requestLayout();
-        // Default state is derived from on/off-text, so state has to be updated when on/off-text
-        // are updated.
-        setOnStateDescription();
+        if (isChecked()) {
+            // Default state is derived from on/off-text, so state has to be updated when
+            // on/off-text are updated.
+            setOnStateDescriptionOnRAndAbove();
+        }
     }
 
     /**
@@ -797,9 +799,11 @@
     public void setTextOff(CharSequence textOff) {
         mTextOff = textOff;
         requestLayout();
-        // Default state is derived from on/off-text, so state has to be updated when on/off-text
-        // are updated.
-        setOffStateDescription();
+        if (!isChecked()) {
+            // Default state is derived from on/off-text, so state has to be updated when
+            // on/off-text are updated.
+            setOffStateDescriptionOnRAndAbove();
+        }
     }
 
     /**
@@ -1095,9 +1099,9 @@
         checked = isChecked();
 
         if (checked) {
-            setOnStateDescription();
+            setOnStateDescriptionOnRAndAbove();
         } else {
-            setOffStateDescription();
+            setOffStateDescriptionOnRAndAbove();
         }
 
         if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
@@ -1433,6 +1437,19 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         info.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+            if (!TextUtils.isEmpty(switchText)) {
+                CharSequence oldText = info.getText();
+                if (TextUtils.isEmpty(oldText)) {
+                    info.setText(switchText);
+                } else {
+                    StringBuilder newText = new StringBuilder();
+                    newText.append(oldText).append(' ').append(switchText);
+                    info.setText(newText);
+                }
+            }
+        }
     }
 
     /**
@@ -1452,17 +1469,21 @@
         return amount < low ? low : (amount > high ? high : amount);
     }
 
-    private void setOnStateDescription() {
-        ViewCompat.setStateDescription(
-                this,
-                mTextOn == null ? getResources().getString(R.string.abc_capital_on) : mTextOn
-        );
+    private void setOnStateDescriptionOnRAndAbove() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            ViewCompat.setStateDescription(
+                    this,
+                    mTextOn == null ? getResources().getString(R.string.abc_capital_on) : mTextOn
+            );
+        }
     }
 
-    private void setOffStateDescription() {
-        ViewCompat.setStateDescription(
-                this,
-                mTextOff == null ? getResources().getString(R.string.abc_capital_off) : mTextOff
-        );
+    private void setOffStateDescriptionOnRAndAbove() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            ViewCompat.setStateDescription(
+                    this,
+                    mTextOff == null ? getResources().getString(R.string.abc_capital_off) : mTextOff
+            );
+        }
     }
 }
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -98,6 +98,7 @@
 
   public interface AppSearchSession {
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
     method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -98,6 +98,7 @@
 
   public interface AppSearchSession {
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
     method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -98,6 +98,7 @@
 
   public interface AppSearchSession {
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
     method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 907de5a..085f8ad 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -56,8 +56,8 @@
     def suffix = name.capitalize()
     project.tasks.create(name: "jar${suffix}", type: Jar) {
         dependsOn variant.javaCompileProvider.get()
-        from variant.javaCompileProvider.get().destinationDir
-        destinationDir new File(project.buildDir, "libJar")
+        from variant.javaCompileProvider.get().destinationDirectory
+        destinationDirectory.set(new File(project.buildDir, "libJar"))
     }
 }
 
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
index 6edc6c1..bb81bfd 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
@@ -27,9 +27,11 @@
 import android.content.Context;
 
 import androidx.appsearch.annotation.AppSearchDocument;
+import androidx.appsearch.app.util.AppSearchTestUtils;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -43,18 +45,22 @@
     @Before
     public void setUp() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
+        AppSearchTestUtils.cleanup(context);
+
         mSession = checkIsResultSuccess(LocalStorage.createSearchSession(
                 new LocalStorage.SearchContext.Builder(context)
-                        .setDatabaseName("testDb1").build()));
+                        .setDatabaseName(AppSearchTestUtils.DB_1).build()));
+    }
 
-        // Remove all documents from any instances that may have been created in the tests.
-        checkIsResultSuccess(
-                mSession.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
+    @After
+    public void tearDown() throws Exception {
+        AppSearchTestUtils.cleanup(ApplicationProvider.getApplicationContext());
     }
 
     @AppSearchDocument
     static class Card {
-        @AppSearchDocument.Uri String mUri;
+        @AppSearchDocument.Uri
+        String mUri;
         @AppSearchDocument.Property
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;        // 3a
@@ -75,48 +81,84 @@
 
     @AppSearchDocument
     static class Gift {
-        @AppSearchDocument.Uri String mUri;
+        @AppSearchDocument.Uri
+        String mUri;
 
         // Collections
-        @AppSearchDocument.Property Collection<Long> mCollectLong;         // 1a
-        @AppSearchDocument.Property Collection<Integer> mCollectInteger;   // 1a
-        @AppSearchDocument.Property Collection<Double> mCollectDouble;     // 1a
-        @AppSearchDocument.Property Collection<Float> mCollectFloat;       // 1a
-        @AppSearchDocument.Property Collection<Boolean> mCollectBoolean;   // 1a
-        @AppSearchDocument.Property Collection<byte[]> mCollectByteArr;    // 1a
-        @AppSearchDocument.Property Collection<String> mCollectString;     // 1b
-        @AppSearchDocument.Property Collection<Card> mCollectCard;         // 1c
+        @AppSearchDocument.Property
+        Collection<Long> mCollectLong;         // 1a
+        @AppSearchDocument.Property
+        Collection<Integer> mCollectInteger;   // 1a
+        @AppSearchDocument.Property
+        Collection<Double> mCollectDouble;     // 1a
+        @AppSearchDocument.Property
+        Collection<Float> mCollectFloat;       // 1a
+        @AppSearchDocument.Property
+        Collection<Boolean> mCollectBoolean;   // 1a
+        @AppSearchDocument.Property
+        Collection<byte[]> mCollectByteArr;    // 1a
+        @AppSearchDocument.Property
+        Collection<String> mCollectString;     // 1b
+        @AppSearchDocument.Property
+        Collection<Card> mCollectCard;         // 1c
 
         // Arrays
-        @AppSearchDocument.Property Long[] mArrBoxLong;         // 2a
-        @AppSearchDocument.Property long[] mArrUnboxLong;       // 2b
-        @AppSearchDocument.Property Integer[] mArrBoxInteger;   // 2a
-        @AppSearchDocument.Property int[] mArrUnboxInt;         // 2a
-        @AppSearchDocument.Property Double[] mArrBoxDouble;     // 2a
-        @AppSearchDocument.Property double[] mArrUnboxDouble;   // 2b
-        @AppSearchDocument.Property Float[] mArrBoxFloat;       // 2a
-        @AppSearchDocument.Property float[] mArrUnboxFloat;     // 2a
-        @AppSearchDocument.Property Boolean[] mArrBoxBoolean;   // 2a
-        @AppSearchDocument.Property boolean[] mArrUnboxBoolean; // 2b
-        @AppSearchDocument.Property byte[][] mArrUnboxByteArr;  // 2b
-        @AppSearchDocument.Property Byte[] mBoxByteArr;         // 2a
-        @AppSearchDocument.Property String[] mArrString;        // 2b
-        @AppSearchDocument.Property Card[] mArrCard;            // 2c
+        @AppSearchDocument.Property
+        Long[] mArrBoxLong;         // 2a
+        @AppSearchDocument.Property
+        long[] mArrUnboxLong;       // 2b
+        @AppSearchDocument.Property
+        Integer[] mArrBoxInteger;   // 2a
+        @AppSearchDocument.Property
+        int[] mArrUnboxInt;         // 2a
+        @AppSearchDocument.Property
+        Double[] mArrBoxDouble;     // 2a
+        @AppSearchDocument.Property
+        double[] mArrUnboxDouble;   // 2b
+        @AppSearchDocument.Property
+        Float[] mArrBoxFloat;       // 2a
+        @AppSearchDocument.Property
+        float[] mArrUnboxFloat;     // 2a
+        @AppSearchDocument.Property
+        Boolean[] mArrBoxBoolean;   // 2a
+        @AppSearchDocument.Property
+        boolean[] mArrUnboxBoolean; // 2b
+        @AppSearchDocument.Property
+        byte[][] mArrUnboxByteArr;  // 2b
+        @AppSearchDocument.Property
+        Byte[] mBoxByteArr;         // 2a
+        @AppSearchDocument.Property
+        String[] mArrString;        // 2b
+        @AppSearchDocument.Property
+        Card[] mArrCard;            // 2c
 
         // Single values
-        @AppSearchDocument.Property String mString;        // 3a
-        @AppSearchDocument.Property Long mBoxLong;         // 3a
-        @AppSearchDocument.Property long mUnboxLong;       // 3b
-        @AppSearchDocument.Property Integer mBoxInteger;   // 3a
-        @AppSearchDocument.Property int mUnboxInt;         // 3b
-        @AppSearchDocument.Property Double mBoxDouble;     // 3a
-        @AppSearchDocument.Property double mUnboxDouble;   // 3b
-        @AppSearchDocument.Property Float mBoxFloat;       // 3a
-        @AppSearchDocument.Property float mUnboxFloat;     // 3b
-        @AppSearchDocument.Property Boolean mBoxBoolean;   // 3a
-        @AppSearchDocument.Property boolean mUnboxBoolean; // 3b
-        @AppSearchDocument.Property byte[] mUnboxByteArr;  // 3a
-        @AppSearchDocument.Property Card mCard;            // 3c
+        @AppSearchDocument.Property
+        String mString;        // 3a
+        @AppSearchDocument.Property
+        Long mBoxLong;         // 3a
+        @AppSearchDocument.Property
+        long mUnboxLong;       // 3b
+        @AppSearchDocument.Property
+        Integer mBoxInteger;   // 3a
+        @AppSearchDocument.Property
+        int mUnboxInt;         // 3b
+        @AppSearchDocument.Property
+        Double mBoxDouble;     // 3a
+        @AppSearchDocument.Property
+        double mUnboxDouble;   // 3b
+        @AppSearchDocument.Property
+        Float mBoxFloat;       // 3a
+        @AppSearchDocument.Property
+        float mUnboxFloat;     // 3b
+        @AppSearchDocument.Property
+        Boolean mBoxBoolean;   // 3a
+        @AppSearchDocument.Property
+        boolean mUnboxBoolean; // 3b
+        @AppSearchDocument.Property
+        byte[] mUnboxByteArr;  // 3a
+        @AppSearchDocument.Property
+        Card mCard;            // 3c
 
         @Override
         public boolean equals(Object other) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
index 99ac18e..746216d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
@@ -24,9 +24,14 @@
 import static org.junit.Assert.assertThrows;
 
 import androidx.appsearch.annotation.AppSearchDocument;
+import androidx.collection.ArrayMap;
 
 import org.junit.Test;
 
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
 public class SetSchemaRequestTest {
 
     @AppSearchDocument
@@ -52,15 +57,24 @@
     }
 
     @Test
-    public void testInvalidSchemaReferences() {
+    public void testInvalidSchemaReferences_fromSystemUiVisibility() {
         IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
-                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(false,
-                        "InvalidSchema").build());
+                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(
+                        "InvalidSchema", false).build());
         assertThat(expected).hasMessageThat().contains("referenced, but were not added");
     }
 
     @Test
-    public void testSchemaTypeVisibilityForSystemUi_Visible() {
+    public void testInvalidSchemaReferences_fromPackageVisibility() {
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForPackage(
+                        "InvalidSchema", /*visible=*/ true, new PackageIdentifier(
+                                "com.foo.package", /*certificate=*/ new byte[]{})).build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForSystemUi_visible() {
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
 
         // By default, the schema is visible.
@@ -70,23 +84,21 @@
 
         request =
                 new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
-                        true,
-                        "Schema").build();
+                        "Schema", true).build();
         assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
     }
 
     @Test
-    public void testSchemaTypeVisibilityForSystemUi_NotVisible() {
+    public void testSchemaTypeVisibilityForSystemUi_notVisible() {
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
         SetSchemaRequest request =
                 new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
-                        false,
-                        "Schema").build();
+                        "Schema", false).build();
         assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Schema");
     }
 
     @Test
-    public void testDataClassVisibilityForSystemUi_Visible() throws Exception {
+    public void testDataClassVisibilityForSystemUi_visible() throws Exception {
         // By default, the schema is visible.
         SetSchemaRequest request =
                 new SetSchemaRequest.Builder().addDataClass(Card.class).build();
@@ -95,18 +107,171 @@
         request =
                 new SetSchemaRequest.Builder().addDataClass(
                         Card.class).setDataClassVisibilityForSystemUi(
-                        true,
-                        Card.class).build();
+                        Card.class, true).build();
         assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
     }
 
     @Test
-    public void testDataClassVisibilityForSystemUi_NotVisible() throws Exception {
+    public void testDataClassVisibilityForSystemUi_notVisible() throws Exception {
         SetSchemaRequest request =
                 new SetSchemaRequest.Builder().addDataClass(
                         Card.class).setDataClassVisibilityForSystemUi(
-                        false,
-                        Card.class).build();
+                        Card.class, false).build();
         assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Card");
     }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_visible() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchema(schema).build();
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+        expectedPackageVisibleMap.put("Schema", Collections.singleton(packageIdentifier));
+
+        request =
+                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
+                        "Schema", /*visible=*/ true, packageIdentifier).build();
+        assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+                expectedPackageVisibleMap);
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_notVisible() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
+                        "Schema", /*visible=*/ false, new PackageIdentifier("com.package.foo",
+                                /*certificate=*/ new byte[]{})).build();
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_deduped() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+        expectedPackageVisibleMap.put("Schema", Collections.singleton(packageIdentifier));
+
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder()
+                        .addSchema(schema)
+                        // Set it visible for "Schema"
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                true, packageIdentifier)
+                        // Set it visible for "Schema" again, which should be a no-op
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                true, packageIdentifier)
+                        .build();
+        assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+                expectedPackageVisibleMap);
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_removed() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder()
+                        .addSchema(schema)
+                        // First set it as visible
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                true, new PackageIdentifier("com.package.foo",
+                                        /*certificate=*/ new byte[]{100}))
+                        // Then make it not visible
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                false, new PackageIdentifier("com.package.foo",
+                                        /*certificate=*/ new byte[]{100}))
+                        .build();
+
+        // Nothing should be visible.
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+    }
+
+    @Test
+    public void testDataClassVisibilityForPackage_visible() throws Exception {
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+        expectedPackageVisibleMap.put("Card", Collections.singleton(packageIdentifier));
+
+        request =
+                new SetSchemaRequest.Builder().addDataClass(
+                        Card.class).setDataClassVisibilityForPackage(
+                        Card.class, /*visible=*/ true, packageIdentifier).build();
+        assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+                expectedPackageVisibleMap);
+    }
+
+    @Test
+    public void testDataClassVisibilityForPackage_notVisible() throws Exception {
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDataClass(
+                        Card.class).setDataClassVisibilityForPackage(
+                        Card.class, /*visible=*/ false,
+                        new PackageIdentifier("com.package.foo", /*certificate=*/
+                                new byte[]{})).build();
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+    }
+
+    @Test
+    public void testDataClassVisibilityForPackage_deduped() throws Exception {
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+        expectedPackageVisibleMap.put("Card", Collections.singleton(packageIdentifier));
+
+        request =
+                new SetSchemaRequest.Builder()
+                        .addDataClass(Card.class)
+                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                                true, packageIdentifier)
+                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                                true, packageIdentifier)
+                        .build();
+        assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+                expectedPackageVisibleMap);
+    }
+
+    @Test
+    public void testDataClassVisibilityForPackage_removed() throws Exception {
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+        request =
+                new SetSchemaRequest.Builder()
+                        .addDataClass(Card.class)
+                        // First set it as visible
+                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                                true, new PackageIdentifier("com.package.foo",
+                                        /*certificate=*/ new byte[]{100}))
+                        // Then make it not visible
+                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                                false, new PackageIdentifier("com.package.foo",
+                                        /*certificate=*/ new byte[]{100}))
+                        .build();
+
+        // Nothing should be visible.
+        assertThat(request.getSchemasPackageAccessible()).isEmpty();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
index ba04b58..7a646b0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
@@ -72,4 +72,105 @@
                         .build()));
         assertThat(e).hasMessageThat().contains("Property defined more than once: subject");
     }
+
+    @Test
+    public void testEquals_identical() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_differentOrder() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build()
+                ).build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure_differentOrder() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        // Order of 'body' and 'subject' has been switched
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
index 9586204..925cff2 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
@@ -40,9 +40,11 @@
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.cts.customer.EmailDataClass;
+import androidx.appsearch.app.util.AppSearchTestUtils;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -53,23 +55,26 @@
 
 public class AppSearchSessionCtsTest {
     private AppSearchSession mDb1;
+    private final String mDbName1 = AppSearchTestUtils.DEFAULT_DATABASE;
     private AppSearchSession mDb2;
+    private final String mDbName2 = AppSearchTestUtils.DB_2;
 
     @Before
     public void setUp() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
+        AppSearchTestUtils.cleanup(context);
+
         mDb1 = checkIsResultSuccess(LocalStorage.createSearchSession(
                 new LocalStorage.SearchContext.Builder(context)
-                        .setDatabaseName("testDb1").build()));
+                        .setDatabaseName(mDbName1).build()));
         mDb2 = checkIsResultSuccess(LocalStorage.createSearchSession(
                 new LocalStorage.SearchContext.Builder(context)
-                        .setDatabaseName("testDb2").build()));
+                        .setDatabaseName(mDbName2).build()));
+    }
 
-        // Remove all documents from any instances that may have been created in the tests.
-        checkIsResultSuccess(
-                mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
-        checkIsResultSuccess(
-                mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
+    @After
+    public void tearDown() throws Exception {
+        AppSearchTestUtils.cleanup(ApplicationProvider.getApplicationContext());
     }
 
     @Test
@@ -99,6 +104,52 @@
     }
 
     @Test
+    public void testGetSchema() throws Exception {
+        AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
+                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2")
+                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest request1 = new SetSchemaRequest.Builder()
+                .addSchema(emailSchema1).addDataClass(EmailDataClass.class).build();
+        SetSchemaRequest request2 = new SetSchemaRequest.Builder()
+                .addSchema(emailSchema2).addDataClass(EmailDataClass.class).build();
+
+        checkIsResultSuccess(mDb1.setSchema(request1));
+        checkIsResultSuccess(mDb2.setSchema(request2));
+
+        Set<AppSearchSchema> actual1 = checkIsResultSuccess(mDb1.getSchema());
+        Set<AppSearchSchema> actual2 = checkIsResultSuccess(mDb2.getSchema());
+
+        assertThat(actual1).isEqualTo(request1.getSchemas());
+        assertThat(actual2).isEqualTo(request2.getSchemas());
+    }
+
+    @Test
     public void testPutDocuments() throws Exception {
         // Schema registration
         checkIsResultSuccess(mDb1.setSchema(
@@ -238,7 +289,7 @@
         assertThat(failResult1.isSuccess()).isFalse();
         assertThat(failResult1.getErrorMessage()).contains("Schema is incompatible");
         assertThat(failResult1.getErrorMessage())
-                .contains("Deleted types: [testDb1/builtin:Email]");
+                .contains("Deleted types: [" + mDbName1 + "/builtin:Email]");
 
         // Try to remove the email schema again, which should now work as we set forceOverride to
         // be true.
@@ -261,10 +312,10 @@
                 .setSubject("testPut example")
                 .build();
         AppSearchBatchResult<String, Void> failResult2 = mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email2).build()).get();
+                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()).get();
         assertThat(failResult2.isSuccess()).isFalse();
         assertThat(failResult2.getFailures().get("email2").getErrorMessage())
-                .isEqualTo("Schema type config 'testDb1/builtin:Email' not found");
+                .isEqualTo("Schema type config '" + mDbName1 + "/builtin:Email' not found");
     }
 
     @Test
@@ -317,7 +368,7 @@
         assertThat(failResult1.isSuccess()).isFalse();
         assertThat(failResult1.getErrorMessage()).contains("Schema is incompatible");
         assertThat(failResult1.getErrorMessage())
-                .contains("Deleted types: [testDb1/builtin:Email]");
+                .contains("Deleted types: [" + mDbName1 + "/builtin:Email]");
 
         // Try to remove the email schema again, which should now work as we set forceOverride to
         // be true.
@@ -341,7 +392,7 @@
                 new PutDocumentsRequest.Builder().addGenericDocument(email3).build()).get();
         assertThat(failResult2.isSuccess()).isFalse();
         assertThat(failResult2.getFailures().get("email3").getErrorMessage())
-                .isEqualTo("Schema type config 'testDb1/builtin:Email' not found");
+                .isEqualTo("Schema type config '" + mDbName1 + "/builtin:Email' not found");
 
         // Make sure email in database 2 still present.
         outDocuments = doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "email2");
@@ -652,7 +703,7 @@
                 new GenericDocument.Builder<>("uri", "Generic")
                         .setNamespace("document")
                         .setPropertyString("subject", "A commonly used fake word is foo. "
-                                        + "Another nonsense word that’s used a lot is bar")
+                                + "Another nonsense word that’s used a lot is bar")
                         .build();
         checkIsBatchResultSuccess(mDb1.putDocuments(
                 new PutDocumentsRequest.Builder().addGenericDocument(document).build()));
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java
index cc1a8bf..ad77545 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java
@@ -35,9 +35,11 @@
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.util.AppSearchTestUtils;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -55,21 +57,23 @@
     @Before
     public void setUp() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
+        AppSearchTestUtils.cleanup(context);
+
         mDb1 = checkIsResultSuccess(LocalStorage.createSearchSession(
                 new LocalStorage.SearchContext.Builder(context)
-                        .setDatabaseName("testDb1").build()));
+                        .setDatabaseName(AppSearchTestUtils.DEFAULT_DATABASE).build()));
         mDb2 = checkIsResultSuccess(LocalStorage.createSearchSession(
                 new LocalStorage.SearchContext.Builder(context)
-                        .setDatabaseName("testDb2").build()));
+                        .setDatabaseName(AppSearchTestUtils.DB_2).build()));
 
         mGlobalAppSearchManager = checkIsResultSuccess(LocalStorage.createGlobalSearchSession(
                 new LocalStorage.GlobalSearchContext.Builder(context).build()));
 
-        // Remove all documents from any instances that may have been created in the tests.
-        checkIsResultSuccess(
-                mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
-        checkIsResultSuccess(
-                mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        AppSearchTestUtils.cleanup(ApplicationProvider.getApplicationContext());
     }
 
     @Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
index fe176c8..0c2a856 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.content.Context;
+
 import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSession;
@@ -25,6 +27,10 @@
 import androidx.appsearch.app.GetByUriRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.localstorage.LocalStorage;
+
+import com.google.common.collect.ImmutableList;
 
 import junit.framework.AssertionFailedError;
 
@@ -34,6 +40,24 @@
 
 public class AppSearchTestUtils {
 
+    // List of databases that may be used in tests. Keeping them in a centralized location helps
+    // #cleanup know which databases to clear.
+    public static final String DEFAULT_DATABASE = LocalStorage.DEFAULT_DATABASE_NAME;
+    public static final String DB_1 = "testDb1";
+    public static final String DB_2 = "testDb2";
+
+    public static void cleanup(Context context) throws Exception {
+        List<String> databases = ImmutableList.of(DEFAULT_DATABASE, DB_1, DB_2);
+        for (String database : databases) {
+            AppSearchSession session =
+                    checkIsResultSuccess(
+                            LocalStorage.createSearchSession(new LocalStorage.SearchContext.Builder(
+                                    context).setDatabaseName(database).build()));
+            checkIsResultSuccess(session.setSchema(
+                    new SetSchemaRequest.Builder().setForceOverride(true).build()));
+        }
+    }
+
     public static <V> V checkIsResultSuccess(Future<AppSearchResult<V>> future) throws Exception {
         AppSearchResult<V> result = future.get();
         if (!result.isSuccess()) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
new file mode 100644
index 0000000..389c3ee
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.util.Size;
+import android.util.SizeF;
+import android.util.SparseArray;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.UUID;
+
+public class BundleUtilTest {
+    @Test
+    public void testDeepEquals_self() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+        assertThat(BundleUtil.deepEquals(one, one)).isTrue();
+    }
+
+    @Test
+    public void testDeepEquals_simple() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+
+        Bundle two = new Bundle();
+        two.putString("a", "a");
+
+        assertThat(one).isNotEqualTo(two);
+        assertThat(BundleUtil.deepEquals(one, two)).isTrue();
+    }
+
+    @Test
+    public void testDeepEquals_keyMismatch() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+
+        Bundle two = new Bundle();
+        two.putString("a", "a");
+        two.putString("b", "b");
+        assertThat(BundleUtil.deepEquals(one, two)).isFalse();
+    }
+
+    @Test
+    public void testDeepEquals_thorough_equal() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            inputs[i] = createThoroughBundle();
+        }
+        assertThat(inputs[0]).isNotEqualTo(inputs[1]);
+        assertThat(BundleUtil.deepEquals(inputs[0], inputs[1])).isTrue();
+    }
+
+    @Test
+    public void testDeepEquals_thorough_notEqual() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            Bundle b = createThoroughBundle();
+            // Create a difference
+            assertThat(b.containsKey("doubleArray")).isTrue();
+            b.putDoubleArray("doubleArray", new double[]{18., i});
+            inputs[i] = b;
+        }
+        assertThat(inputs[0]).isNotEqualTo(inputs[1]);
+        assertThat(BundleUtil.deepEquals(inputs[0], inputs[1])).isFalse();
+    }
+
+    @Test
+    public void testDeepEquals_nestedNotEquals() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+        Bundle two = new Bundle();
+        two.putBundle("b", one);
+        Bundle twoClone = new Bundle();
+        twoClone.putBundle("b", one);
+        Bundle three = new Bundle();
+        three.putBundle("b", two);
+
+        ArrayList<Bundle> listOne = new ArrayList<>(ImmutableList.of(one, two, three));
+        ArrayList<Bundle> listOneClone = new ArrayList<>(ImmutableList.of(one, twoClone, three));
+        ArrayList<Bundle> listTwo = new ArrayList<>(ImmutableList.of(one, three, two));
+        Bundle b1 = new Bundle();
+        b1.putParcelableArrayList("key", listOne);
+        Bundle b1Clone = new Bundle();
+        b1Clone.putParcelableArrayList("key", listOneClone);
+        Bundle b2 = new Bundle();
+        b2.putParcelableArrayList("key", listTwo);
+
+        assertThat(b1).isNotEqualTo(b1Clone);
+        assertThat(BundleUtil.deepEquals(b1, b1Clone)).isTrue();
+        assertThat(BundleUtil.deepEquals(b1, b2)).isFalse();
+        assertThat(BundleUtil.deepEquals(b1Clone, b2)).isFalse();
+    }
+
+    @Test
+    public void testDeepEquals_sparseArray() {
+        Parcelable parcelable1 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable2 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable3 = new ParcelUuid(UUID.randomUUID());
+
+        SparseArray<Parcelable> array1 = new SparseArray<>();
+        array1.put(1, parcelable1);
+        array1.put(10, parcelable2);
+
+        SparseArray<Parcelable> array1Clone = new SparseArray<>();
+        array1Clone.put(1, parcelable1);
+        array1Clone.put(10, parcelable2);
+
+        SparseArray<Parcelable> array2 = new SparseArray<>();
+        array2.put(1, parcelable1);
+        array2.put(10, parcelable3);  // Different
+
+        Bundle b1 = new Bundle();
+        b1.putSparseParcelableArray("array1", array1);
+        Bundle b1Clone = new Bundle();
+        b1Clone.putSparseParcelableArray("array1", array1Clone);
+        Bundle b2 = new Bundle();
+        b2.putSparseParcelableArray("array1", array2);
+
+        assertThat(b1).isNotEqualTo(b1Clone);
+        assertThat(BundleUtil.deepEquals(b1, b1Clone)).isTrue();
+        assertThat(BundleUtil.deepEquals(b1, b2)).isFalse();
+        assertThat(BundleUtil.deepEquals(b1Clone, b2)).isFalse();
+    }
+
+    @Test
+    public void testDeepHashCode_same() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            inputs[i] = createThoroughBundle();
+        }
+        assertThat(BundleUtil.deepHashCode(inputs[0]))
+                .isEqualTo(BundleUtil.deepHashCode(inputs[1]));
+    }
+
+    @Test
+    public void testDeepHashCode_different() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            Bundle b = createThoroughBundle();
+            // Create a difference
+            assertThat(b.containsKey("doubleArray")).isTrue();
+            b.putDoubleArray("doubleArray", new double[]{18., i});
+            inputs[i] = b;
+        }
+        assertThat(BundleUtil.deepHashCode(inputs[0]))
+                .isNotEqualTo(BundleUtil.deepHashCode(inputs[1]));
+    }
+
+    @Test
+    public void testHashCode_sparseArray() {
+        Parcelable parcelable1 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable2 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable3 = new ParcelUuid(UUID.randomUUID());
+
+        SparseArray<Parcelable> array1 = new SparseArray<>();
+        array1.put(1, parcelable1);
+        array1.put(10, parcelable2);
+
+        SparseArray<Parcelable> array1Clone = new SparseArray<>();
+        array1Clone.put(1, parcelable1);
+        array1Clone.put(10, parcelable2);
+
+        SparseArray<Parcelable> array2 = new SparseArray<>();
+        array2.put(1, parcelable1);
+        array2.put(10, parcelable3);  // Different
+
+        Bundle b1 = new Bundle();
+        b1.putSparseParcelableArray("array1", array1);
+        Bundle b1Clone = new Bundle();
+        b1Clone.putSparseParcelableArray("array1", array1Clone);
+        Bundle b2 = new Bundle();
+        b2.putSparseParcelableArray("array1", array2);
+
+        assertThat(b1.hashCode()).isNotEqualTo(b1Clone.hashCode());
+        assertThat(BundleUtil.deepHashCode(b1)).isEqualTo(BundleUtil.deepHashCode(b1Clone));
+        assertThat(BundleUtil.deepHashCode(b1)).isNotEqualTo(BundleUtil.deepHashCode(b2));
+    }
+
+    private static Bundle createThoroughBundle() {
+        Bundle toy1 = new Bundle();
+        toy1.putString("a", "a");
+        Bundle toy2 = new Bundle();
+        toy2.putInt("b", 2);
+
+        Bundle b = new Bundle();
+        // BaseBundle stuff
+        b.putBoolean("boolean", true);
+        b.putByte("byte", (byte) 1);
+        b.putChar("char", 'a');
+        b.putShort("short", (short) 2);
+        b.putInt("int", 3);
+        b.putLong("long", 4L);
+        b.putFloat("float", 5f);
+        b.putDouble("double", 6f);
+        b.putString("string", "b");
+        b.putCharSequence("charSequence", "c");
+        b.putIntegerArrayList("integerArrayList", new ArrayList<>(ImmutableList.of(7, 8)));
+        b.putStringArrayList("stringArrayList", new ArrayList<>(ImmutableList.of("d", "e")));
+        b.putCharSequenceArrayList(
+                "charSequenceArrayList", new ArrayList<>(ImmutableList.of("f", "g")));
+        b.putSerializable("serializable", new BigDecimal(9));
+        b.putBooleanArray("booleanArray", new boolean[]{true, false, true});
+        b.putByteArray("byteArray", new byte[]{(byte) 10, (byte) 11});
+        b.putShortArray("shortArray", new short[]{(short) 12, (short) 13});
+        b.putCharArray("charArray", new char[]{'h', 'i'});
+        b.putLongArray("longArray", new long[]{14L, 15L});
+        b.putFloatArray("floatArray", new float[]{16f, 17f});
+        b.putDoubleArray("doubleArray", new double[]{18., 19.});
+        b.putStringArray("stringArray", new String[]{"j", "k"});
+        b.putCharSequenceArray("charSequenceArrayList", new CharSequence[]{"l", "m"});
+
+        // Bundle stuff
+        b.putParcelable("parcelable", toy1);
+        if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+            b.putSize("size", new Size(20, 21));
+            b.putSizeF("sizeF", new SizeF(22f, 23f));
+        }
+        b.putParcelableArray("parcelableArray", new Parcelable[]{toy1, toy2});
+        b.putParcelableArrayList(
+                "parcelableArrayList", new ArrayList<>(ImmutableList.of(toy1, toy2)));
+        SparseArray<Parcelable> sparseArray = new SparseArray<>();
+        sparseArray.put(24, toy1);
+        sparseArray.put(1025, toy2);
+        b.putSparseParcelableArray("sparceParcelableArray", sparseArray);
+        b.putBundle("bundle", toy1);
+
+        return b;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index 9d0460a..11b587b 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -24,7 +24,9 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.appsearch.util.BundleUtil;
 import androidx.collection.ArraySet;
+import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -96,17 +98,37 @@
         return ret;
     }
 
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof AppSearchSchema)) {
+            return false;
+        }
+        AppSearchSchema otherSchema = (AppSearchSchema) other;
+        if (!getSchemaType().equals(otherSchema.getSchemaType())) {
+            return false;
+        }
+        return getProperties().equals(otherSchema.getProperties());
+    }
+
+    @Override
+    public int hashCode() {
+        return ObjectsCompat.hash(getSchemaType(), getProperties());
+    }
+
     /** Builder for {@link AppSearchSchema objects}. */
     public static final class Builder {
-        private final String mTypeName;
+        private final String mSchemaType;
         private final ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
         private final Set<String> mPropertyNames = new ArraySet<>();
         private boolean mBuilt = false;
 
         /** Creates a new {@link AppSearchSchema.Builder}. */
-        public Builder(@NonNull String typeName) {
-            Preconditions.checkNotNull(typeName);
-            mTypeName = typeName;
+        public Builder(@NonNull String schemaType) {
+            Preconditions.checkNotNull(schemaType);
+            mSchemaType = schemaType;
         }
 
         /** Adds a property to the given type. */
@@ -135,7 +157,7 @@
         public AppSearchSchema build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             Bundle bundle = new Bundle();
-            bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mTypeName);
+            bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
             bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
             mBuilt = true;
             return new AppSearchSchema(bundle);
@@ -273,6 +295,9 @@
 
         final Bundle mBundle;
 
+        @Nullable
+        private Integer mHashCode;
+
         PropertyConfig(@NonNull Bundle bundle) {
             mBundle = Preconditions.checkNotNull(bundle);
         }
@@ -321,6 +346,26 @@
             return mBundle.getInt(TOKENIZER_TYPE_FIELD);
         }
 
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof PropertyConfig)) {
+                return false;
+            }
+            PropertyConfig otherProperty = (PropertyConfig) other;
+            return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+        }
+
+        @Override
+        public int hashCode() {
+            if (mHashCode == null) {
+                mHashCode = BundleUtil.deepHashCode(mBundle);
+            }
+            return mHashCode;
+        }
+
         /**
          * Builder for {@link PropertyConfig}.
          *
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index b4a962e..0a46469 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -16,10 +16,14 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.Set;
+
 /**
  * Represents a connection to an AppSearch storage system where {@link GenericDocument}s can be
  * placed and queried.
@@ -29,7 +33,7 @@
 public interface AppSearchSession {
 
     /**
-     * Sets the schema being used by documents provided to the {@link #putDocuments} method.
+     * Sets the schema that will be used by documents provided to the {@link #putDocuments} method.
      *
      * <p>The schema provided here is compared to the stored copy of the schema previously supplied
      * to {@link #setSchema}, if any, to determine how to treat existing documents. The following
@@ -88,6 +92,16 @@
     ListenableFuture<AppSearchResult<Void>> setSchema(@NonNull SetSchemaRequest request);
 
     /**
+     * Retrieves the schema most recently successfully provided to {@link #setSchema}.
+     *
+     * @return The pending result of performing this operation.
+     */
+    // This call hits disk; async API prevents us from treating these calls as properties.
+    @SuppressLint("KotlinPropertyAccess")
+    @NonNull
+    ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema();
+
+    /**
      * Indexes documents into AppSearch.
      *
      * <p>Each {@link GenericDocument}'s {@code schemaType} field must be set to the name of a
@@ -178,11 +192,12 @@
 
     /**
      * Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
-     * match the query expression in given namespaces and schemaTypes.
+     * match the {@code queryExpression} in given namespaces and schemaTypes which is set via
+     * {@link SearchSpec.Builder#addNamespace} and {@link SearchSpec.Builder#addSchemaType}.
      *
-     * <p> An empty query matches all documents.
+     * <p> An empty {@code queryExpression} matches all documents.
      *
-     * <p> An empty set of namespaces or of schemaTypes matches all namespaces or schemaTypes in
+     * <p> An empty set of namespaces or schemaTypes matches all namespaces or schemaTypes in
      * the current database.
      *
      * @param queryExpression Query String to search.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index ca34d32..28357af 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -25,6 +25,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.util.BundleUtil;
 import androidx.core.util.Preconditions;
 
 import java.lang.reflect.Array;
@@ -43,7 +44,7 @@
  * @see AppSearchSession#query
  */
 public class GenericDocument {
-    private static final String TAG = "GenericDocument";
+    private static final String TAG = "AppSearchGenericDocumen";
 
     /** The default empty namespace.*/
     public static final String DEFAULT_NAMESPACE = "";
@@ -480,142 +481,17 @@
             return false;
         }
         GenericDocument otherDocument = (GenericDocument) other;
-        return bundleEquals(this.mBundle, otherDocument.mBundle);
-    }
-
-    /**
-     * Deeply checks whether two bundles are equal.
-     * <p>Two bundles will be considered equal if they contain the same content.
-     */
-    @SuppressWarnings("unchecked")
-    private static boolean bundleEquals(Bundle one, Bundle two) {
-        if (one.size() != two.size()) {
-            return false;
-        }
-        Set<String> keySetOne = one.keySet();
-        Object valueOne;
-        Object valueTwo;
-        // Bundle inherit its equals() from Object.java, which only compare their memory address.
-        // We should iterate all keys and check their presents and values in both bundle.
-        for (String key : keySetOne) {
-            valueOne = one.get(key);
-            valueTwo = two.get(key);
-            if (valueOne instanceof Bundle
-                    && valueTwo instanceof Bundle
-                    && !bundleEquals((Bundle) valueOne, (Bundle) valueTwo)) {
-                return false;
-            } else if (valueOne == null && (valueTwo != null || !two.containsKey(key))) {
-                // If we call bundle.get(key) when the 'key' doesn't actually exist in the
-                // bundle, we'll get back a null. So make sure that both values are null and
-                // both keys exist in the bundle.
-                return false;
-            } else if (valueOne instanceof boolean[]) {
-                if (!(valueTwo instanceof boolean[])
-                        || !Arrays.equals((boolean[]) valueOne, (boolean[]) valueTwo)) {
-                    return false;
-                }
-            } else if (valueOne instanceof long[]) {
-                if (!(valueTwo instanceof long[])
-                        || !Arrays.equals((long[]) valueOne, (long[]) valueTwo)) {
-                    return false;
-                }
-            } else if (valueOne instanceof double[]) {
-                if (!(valueTwo instanceof double[])
-                        || !Arrays.equals((double[]) valueOne, (double[]) valueTwo)) {
-                    return false;
-                }
-            } else if (valueOne instanceof Bundle[]) {
-                if (!(valueTwo instanceof Bundle[])) {
-                    return false;
-                }
-                Bundle[] bundlesOne = (Bundle[]) valueOne;
-                Bundle[] bundlesTwo = (Bundle[]) valueTwo;
-                if (bundlesOne.length != bundlesTwo.length) {
-                    return false;
-                }
-                for (int i = 0; i < bundlesOne.length; i++) {
-                    if (!bundleEquals(bundlesOne[i], bundlesTwo[i])) {
-                        return false;
-                    }
-                }
-            } else if (valueOne instanceof ArrayList) {
-                if (!(valueTwo instanceof ArrayList)) {
-                    return false;
-                }
-                ArrayList<Bundle> bundlesOne = (ArrayList<Bundle>) valueOne;
-                ArrayList<Bundle> bundlesTwo = (ArrayList<Bundle>) valueTwo;
-                if (bundlesOne.size() != bundlesTwo.size()) {
-                    return false;
-                }
-                for (int i = 0; i < bundlesOne.size(); i++) {
-                    if (!bundleEquals(bundlesOne.get(i), bundlesTwo.get(i))) {
-                        return false;
-                    }
-                }
-            } else if (valueOne instanceof Object[]) {
-                if (!(valueTwo instanceof Object[])
-                        || !Arrays.equals((Object[]) valueOne, (Object[]) valueTwo)) {
-                    return false;
-                }
-            }
-        }
-        return true;
+        return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle);
     }
 
     @Override
     public int hashCode() {
         if (mHashCode == null) {
-            mHashCode = bundleHashCode(mBundle);
+            mHashCode = BundleUtil.deepHashCode(mBundle);
         }
         return mHashCode;
     }
 
-    /**
-     * Calculates the hash code for a bundle.
-     * <p> The hash code is only effected by the contents in the bundle. Bundles will get
-     * consistent hash code if they have same contents.
-     */
-    @SuppressWarnings("unchecked")
-    private static int bundleHashCode(Bundle bundle) {
-        int[] hashCodes = new int[bundle.size()];
-        int i = 0;
-        // Bundle inherit its hashCode() from Object.java, which only relative to their memory
-        // address. Bundle doesn't have an order, so we should iterate all keys and combine
-        // their value's hashcode into an array. And use the hashcode of the array to be
-        // the hashcode of the bundle.
-        for (String key : bundle.keySet()) {
-            Object value = bundle.get(key);
-            if (value instanceof boolean[]) {
-                hashCodes[i++] = Arrays.hashCode((boolean[]) value);
-            } else if (value instanceof long[]) {
-                hashCodes[i++] = Arrays.hashCode((long[]) value);
-            } else if (value instanceof double[]) {
-                hashCodes[i++] = Arrays.hashCode((double[]) value);
-            } else if (value instanceof String[]) {
-                hashCodes[i++] = Arrays.hashCode((Object[]) value);
-            } else if (value instanceof Bundle) {
-                hashCodes[i++] = bundleHashCode((Bundle) value);
-            } else if (value instanceof Bundle[]) {
-                Bundle[] bundles = (Bundle[]) value;
-                int[] innerHashCodes = new int[bundles.length];
-                for (int j = 0; j < innerHashCodes.length; j++) {
-                    innerHashCodes[j] = bundleHashCode(bundles[j]);
-                }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
-            } else if (value instanceof ArrayList) {
-                ArrayList<Bundle> bundles = (ArrayList<Bundle>) value;
-                int[] innerHashCodes = new int[bundles.size()];
-                for (int j = 0; j < innerHashCodes.length; j++) {
-                    innerHashCodes[j] = bundleHashCode(bundles.get(j));
-                }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
-            } else {
-                hashCodes[i++] = value.hashCode();
-            }
-        }
-        return Arrays.hashCode(hashCodes);
-    }
-
     @Override
     @NonNull
     public String toString() {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
new file mode 100644
index 0000000..a1ab3b5
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.ObjectsCompat;
+
+import java.util.Arrays;
+
+/**
+ * This class represents a uniquely identifiable package.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class PackageIdentifier {
+    public final String packageName;
+    public final byte[] certificate;
+
+    /**
+     * Creates a unique identifier for a package.
+     *
+     * @param packageName Name of the package.
+     * @param certificate SHA256 certificate digest of the package.
+     */
+    public PackageIdentifier(@NonNull String packageName, @NonNull byte[] certificate) {
+        this.packageName = packageName;
+        this.certificate = certificate;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || !(obj instanceof PackageIdentifier)) {
+            return false;
+        }
+        final PackageIdentifier other = (PackageIdentifier) obj;
+        return this.packageName.equals(other.packageName)
+                && Arrays.equals(this.certificate, other.certificate);
+    }
+
+    @Override
+    public int hashCode() {
+        return ObjectsCompat.hash(packageName, Arrays.hashCode(certificate));
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
index 4093c8b..ed7cad9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
@@ -45,7 +45,7 @@
         return mNamespace;
     }
 
-    /** Returns the URIs to remove from the namespace. */
+    /** Returns the URIs of documents to remove from the namespace. */
     @NonNull
     public Set<String> getUris() {
         return Collections.unmodifiableSet(mUris);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index a918106..04be1e0 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -21,6 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
@@ -29,6 +30,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -39,12 +41,16 @@
 public final class SetSchemaRequest {
     private final Set<AppSearchSchema> mSchemas;
     private final Set<String> mSchemasNotPlatformSurfaceable;
+    private final Map<String, Set<PackageIdentifier>> mSchemasPackageAccessible;
     private final boolean mForceOverride;
 
     SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
-            @NonNull Set<String> schemasNotPlatformSurfaceable, boolean forceOverride) {
+            @NonNull Set<String> schemasNotPlatformSurfaceable,
+            @NonNull Map<String, Set<PackageIdentifier>> schemasPackageAccessible,
+            boolean forceOverride) {
         mSchemas = Preconditions.checkNotNull(schemas);
         mSchemasNotPlatformSurfaceable = Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
+        mSchemasPackageAccessible = Preconditions.checkNotNull(schemasPackageAccessible);
         mForceOverride = forceOverride;
     }
 
@@ -65,6 +71,42 @@
         return Collections.unmodifiableSet(mSchemasNotPlatformSurfaceable);
     }
 
+    /**
+     * Returns a mapping of schema types to the set of packages that have access
+     * to that schema type. Each package is represented by a {@link PackageIdentifier}.
+     * name and byte[] certificate.
+     *
+     * This method is inefficient to call repeatedly.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Map<String, Set<PackageIdentifier>> getSchemasPackageAccessible() {
+        Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
+        for (String key : mSchemasPackageAccessible.keySet()) {
+            copy.put(key, new ArraySet<>(mSchemasPackageAccessible.get(key)));
+        }
+        return copy;
+    }
+
+    /**
+     * Returns a mapping of schema types to the set of packages that have access
+     * to that schema type. Each package is represented by a {@link PackageIdentifier}.
+     * name and byte[] certificate.
+     *
+     * A more efficient version of {@code #getSchemasPackageAccessible}, but it returns a
+     * modifiable map. This is not meant to be unhidden and should only be used by internal
+     * classes.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Map<String, Set<PackageIdentifier>> getSchemasPackageAccessibleInternal() {
+        return mSchemasPackageAccessible;
+    }
+
     /** Returns whether this request will force the schema to be overridden. */
     public boolean isForceOverride() {
         return mForceOverride;
@@ -74,6 +116,8 @@
     public static final class Builder {
         private final Set<AppSearchSchema> mSchemas = new ArraySet<>();
         private final Set<String> mSchemasNotPlatformSurfaceable = new ArraySet<>();
+        private final Map<String, Set<PackageIdentifier>> mSchemasPackageAccessible =
+                new ArrayMap<>();
         private boolean mForceOverride = false;
         private boolean mBuilt = false;
 
@@ -145,78 +189,112 @@
         }
 
         /**
-         * Sets visibility on system UI surfaces for schema types.
+         * Sets visibility on system UI surfaces for the given {@code schemaType}.
          *
+         * @param schemaType The schema type to set visibility on.
+         * @param visible    Whether the {@code schemaType} will be visible or not.
          * @hide
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @NonNull
-        public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
-                @NonNull String... schemaTypes) {
-            Preconditions.checkNotNull(schemaTypes);
-            return this.setSchemaTypeVisibilityForSystemUi(visible, Arrays.asList(schemaTypes));
-        }
-
-        /**
-         * Sets visibility on system UI surfaces for schema types.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
-                @NonNull Collection<String> schemaTypes) {
+        public Builder setSchemaTypeVisibilityForSystemUi(@NonNull String schemaType,
+                boolean visible) {
+            Preconditions.checkNotNull(schemaType);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(schemaTypes);
+
             if (visible) {
-                mSchemasNotPlatformSurfaceable.removeAll(schemaTypes);
+                mSchemasNotPlatformSurfaceable.remove(schemaType);
             } else {
-                mSchemasNotPlatformSurfaceable.addAll(schemaTypes);
+                mSchemasNotPlatformSurfaceable.add(schemaType);
             }
             return this;
         }
 
         /**
-         * Sets visibility on system UI surfaces for schema types.
+         * Sets visibility for a package for the given {@code schemaType}.
          *
-         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         * @param schemaType        The schema type to set visibility on.
+         * @param visible           Whether the {@code schemaType} will be visible or not.
+         * @param packageIdentifier Represents the package that will be granted visibility.
          * @hide
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @NonNull
-        public Builder setDataClassVisibilityForSystemUi(boolean visible,
-                @NonNull Class<?>... dataClasses) throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            return setDataClassVisibilityForSystemUi(visible, Arrays.asList(dataClasses));
-        }
-
-        /**
-         * Sets visibility on system UI surfaces for schema types.
-         *
-         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setDataClassVisibilityForSystemUi(boolean visible,
-                @NonNull Collection<Class<?>> dataClasses) throws AppSearchException {
+        public Builder setSchemaTypeVisibilityForPackage(@NonNull String schemaType,
+                boolean visible, @NonNull PackageIdentifier packageIdentifier) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(packageIdentifier);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(dataClasses);
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            for (Class<?> dataClass : dataClasses) {
-                DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
-                if (visible) {
-                    mSchemasNotPlatformSurfaceable.remove(factory.getSchemaType());
-                } else {
-                    mSchemasNotPlatformSurfaceable.add(factory.getSchemaType());
+
+            Set<PackageIdentifier> packageIdentifiers =
+                    mSchemasPackageAccessible.get(schemaType);
+            if (visible) {
+                if (packageIdentifiers == null) {
+                    packageIdentifiers = new ArraySet<>();
+                }
+                packageIdentifiers.add(packageIdentifier);
+                mSchemasPackageAccessible.put(schemaType, packageIdentifiers);
+            } else {
+                if (packageIdentifiers == null) {
+                    // Return early since there was nothing set to begin with.
+                    return this;
+                }
+                packageIdentifiers.remove(packageIdentifier);
+                if (packageIdentifiers.isEmpty()) {
+                    // Remove the entire key so that we don't have empty sets as values.
+                    mSchemasPackageAccessible.remove(schemaType);
                 }
             }
+
             return this;
         }
 
         /**
+         * Sets visibility on system UI surfaces for the given {@code dataClass}.
+         *
+         * @param dataClass The schema to set visibility on.
+         * @param visible   Whether the {@code schemaType} will be visible or not.
+         * @return {@link SetSchemaRequest.Builder}
+         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+         *                            has not generated a schema for the given data classes.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setDataClassVisibilityForSystemUi(@NonNull Class<?> dataClass,
+                boolean visible) throws AppSearchException {
+            Preconditions.checkNotNull(dataClass);
+
+            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
+            DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+            return setSchemaTypeVisibilityForSystemUi(factory.getSchemaType(), visible);
+        }
+
+        /**
+         * Sets visibility for a package for the given {@code dataClass}.
+         *
+         * @param dataClass         The schema to set visibility on.
+         * @param visible           Whether the {@code schemaType} will be visible or not.
+         * @param packageIdentifier Represents the package that will be granted visibility
+         * @return {@link SetSchemaRequest.Builder}
+         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+         *                            has not generated a schema for the given data classes.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setDataClassVisibilityForPackage(@NonNull Class<?> dataClass,
+                boolean visible, @NonNull PackageIdentifier packageIdentifier)
+                throws AppSearchException {
+            Preconditions.checkNotNull(dataClass);
+
+            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
+            DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+            return setSchemaTypeVisibilityForPackage(factory.getSchemaType(), visible,
+                    packageIdentifier);
+        }
+
+        /**
          * Configures the {@link SetSchemaRequest} to delete any existing documents that don't
          * follow the new schema.
          *
@@ -244,20 +322,23 @@
 
             // Verify that any schema types with visibility settings refer to a real schema.
             // Create a copy because we're going to remove from the set for verification purposes.
-            Set<String> schemasNotPlatformSurfaceableCopy = new ArraySet<>(
+            Set<String> referencedSchemas = new ArraySet<>(
                     mSchemasNotPlatformSurfaceable);
+            referencedSchemas.addAll(mSchemasPackageAccessible.keySet());
+
             for (AppSearchSchema schema : mSchemas) {
-                schemasNotPlatformSurfaceableCopy.remove(schema.getSchemaType());
+                referencedSchemas.remove(schema.getSchemaType());
             }
-            if (!schemasNotPlatformSurfaceableCopy.isEmpty()) {
+            if (!referencedSchemas.isEmpty()) {
                 // We still have schema types that weren't seen in our mSchemas set. This means
                 // there wasn't a corresponding AppSearchSchema.
                 throw new IllegalArgumentException(
-                        "Schema types " + schemasNotPlatformSurfaceableCopy
+                        "Schema types " + referencedSchemas
                                 + " referenced, but were not added.");
             }
 
             return new SetSchemaRequest(mSchemas, mSchemasNotPlatformSurfaceable,
+                    mSchemasPackageAccessible,
                     mForceOverride);
         }
     }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
new file mode 100644
index 0000000..86de233
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.util;
+
+import android.os.Bundle;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Utilities for working with {@link android.os.Bundle}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class BundleUtil {
+    private BundleUtil() {}
+
+    /**
+     * Deeply checks two bundles are equal or not.
+     *
+     * <p>Two bundles will be considered equal if they contain the same keys, and each value is also
+     * equal. Bundle values are compared using deepEquals.
+     */
+    public static boolean deepEquals(@Nullable Bundle one, @Nullable Bundle two) {
+        if (one == null && two == null) {
+            return true;
+        }
+        if (one == null || two == null) {
+            return false;
+        }
+        if (one.size() != two.size()) {
+            return false;
+        }
+        if (!one.keySet().equals(two.keySet())) {
+            return false;
+        }
+        // Bundle inherit its equals() from Object.java, which only compare their memory address.
+        // We should iterate all keys and check their presents and values in both bundle.
+        for (String key : one.keySet()) {
+            if (!bundleValueEquals(one.get(key), two.get(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Deeply checks whether two values in a Bundle are equal or not.
+     *
+     * <p>Values of type Bundle are compared using {@link #deepEquals}.
+     */
+    private static boolean bundleValueEquals(@Nullable Object one, @Nullable Object two) {
+        if (one == null && two == null) {
+            return true;
+        }
+        if (one == null || two == null) {
+            return false;
+        }
+        if (one.equals(two)) {
+            return true;
+        }
+        if (one instanceof Bundle && two instanceof Bundle) {
+            return deepEquals((Bundle) one, (Bundle) two);
+        } else if (one instanceof int[] && two instanceof int[]) {
+            return Arrays.equals((int[]) one, (int[]) two);
+        } else if (one instanceof byte[] && two instanceof byte[]) {
+            return Arrays.equals((byte[]) one, (byte[]) two);
+        } else if (one instanceof char[] && two instanceof char[]) {
+            return Arrays.equals((char[]) one, (char[]) two);
+        } else if (one instanceof long[] && two instanceof long[]) {
+            return Arrays.equals((long[]) one, (long[]) two);
+        } else if (one instanceof float[] && two instanceof float[]) {
+            return Arrays.equals((float[]) one, (float[]) two);
+        } else if (one instanceof short[] && two instanceof short[]) {
+            return Arrays.equals((short[]) one, (short[]) two);
+        } else if (one instanceof double[] && two instanceof double[]) {
+            return Arrays.equals((double[]) one, (double[]) two);
+        } else if (one instanceof boolean[] && two instanceof boolean[]) {
+            return Arrays.equals((boolean[]) one, (boolean[]) two);
+        } else if (one instanceof Object[] && two instanceof Object[]) {
+            Object[] arrayOne = (Object[]) one;
+            Object[] arrayTwo = (Object[]) two;
+            if (arrayOne.length != arrayTwo.length) {
+                return false;
+            }
+            if (Arrays.equals(arrayOne, arrayTwo)) {
+                return true;
+            }
+            for (int i = 0; i < arrayOne.length; i++) {
+                if (!bundleValueEquals(arrayOne[i], arrayTwo[i])) {
+                    return false;
+                }
+            }
+            return true;
+        } else if (one instanceof ArrayList && two instanceof ArrayList) {
+            ArrayList<?> listOne = (ArrayList<?>) one;
+            ArrayList<?> listTwo = (ArrayList<?>) two;
+            if (listOne.size() != listTwo.size()) {
+                return false;
+            }
+            for (int i = 0; i < listOne.size(); i++) {
+                if (!bundleValueEquals(listOne.get(i), listTwo.get(i))) {
+                    return false;
+                }
+            }
+            return true;
+        } else if (one instanceof SparseArray && two instanceof SparseArray) {
+            SparseArray<?> arrayOne = (SparseArray<?>) one;
+            SparseArray<?> arrayTwo = (SparseArray<?>) two;
+            if (arrayOne.size() != arrayTwo.size()) {
+                return false;
+            }
+            for (int i = 0; i < arrayOne.size(); i++) {
+                if (arrayOne.keyAt(i) != arrayTwo.keyAt(i)
+                        || !bundleValueEquals(arrayOne.valueAt(i), arrayTwo.valueAt(i))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Calculates the hash code for a bundle.
+     * <p> The hash code is only effected by the contents in the bundle. Bundles will get
+     * consistent hash code if they have same contents.
+     */
+    public static int deepHashCode(@Nullable Bundle bundle) {
+        if (bundle == null) {
+            return 0;
+        }
+        int[] hashCodes = new int[bundle.size()];
+        int i = 0;
+        // Bundle inherit its hashCode() from Object.java, which only relative to their memory
+        // address. Bundle doesn't have an order, so we should iterate all keys and combine
+        // their value's hashcode into an array. And use the hashcode of the array to be
+        // the hashcode of the bundle.
+        for (String key : bundle.keySet()) {
+            Object value = bundle.get(key);
+            if (value instanceof Bundle) {
+                hashCodes[i++] = deepHashCode((Bundle) value);
+            } else if (value instanceof int[]) {
+                hashCodes[i++] = Arrays.hashCode((int[]) value);
+            } else if (value instanceof byte[]) {
+                hashCodes[i++] = Arrays.hashCode((byte[]) value);
+            } else if (value instanceof char[]) {
+                hashCodes[i++] = Arrays.hashCode((char[]) value);
+            } else if (value instanceof long[]) {
+                hashCodes[i++] = Arrays.hashCode((long[]) value);
+            } else if (value instanceof float[]) {
+                hashCodes[i++] = Arrays.hashCode((float[]) value);
+            } else if (value instanceof short[]) {
+                hashCodes[i++] = Arrays.hashCode((short[]) value);
+            } else if (value instanceof double[]) {
+                hashCodes[i++] = Arrays.hashCode((double[]) value);
+            } else if (value instanceof boolean[]) {
+                hashCodes[i++] = Arrays.hashCode((boolean[]) value);
+            } else if (value instanceof String[]) {
+                // Optimization to avoid Object[] handler creating an inner array for common cases
+                hashCodes[i++] = Arrays.hashCode((String[]) value);
+            } else if (value instanceof Object[]) {
+                Object[] array = (Object[]) value;
+                int[] innerHashCodes = new int[array.length];
+                for (int j = 0; j < array.length; j++) {
+                    if (array[j] instanceof Bundle) {
+                        innerHashCodes[j] = deepHashCode((Bundle) array[j]);
+                    } else if (array[j] != null) {
+                        innerHashCodes[j] = array[j].hashCode();
+                    }
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else if (value instanceof ArrayList) {
+                ArrayList<?> list = (ArrayList<?>) value;
+                int[] innerHashCodes = new int[list.size()];
+                for (int j = 0; j < innerHashCodes.length; j++) {
+                    Object item = list.get(j);
+                    if (item instanceof Bundle) {
+                        innerHashCodes[j] = deepHashCode((Bundle) item);
+                    } else if (item != null) {
+                        innerHashCodes[j] = item.hashCode();
+                    }
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else if (value instanceof SparseArray) {
+                SparseArray<?> array = (SparseArray<?>) value;
+                int[] innerHashCodes = new int[array.size() * 2];
+                for (int j = 0; j < array.size(); j++) {
+                    innerHashCodes[j * 2] = array.keyAt(j);
+                    Object item = array.valueAt(j);
+                    if (item instanceof Bundle) {
+                        innerHashCodes[j * 2 + 1] = deepHashCode((Bundle) item);
+                    } else if (item != null) {
+                        innerHashCodes[j * 2 + 1] = item.hashCode();
+                    }
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else {
+                hashCodes[i++] = value.hashCode();
+            }
+        }
+        return Arrays.hashCode(hashCodes);
+    }
+}
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index 33f9429..1a646e2 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -32,7 +32,7 @@
 JETPACK_IMPL_TEST_ROOT = 'local-storage/src/androidTest/java/androidx/appsearch'
 
 # Framework paths relative to frameworks/base/apex/appsearch
-FRAMEWORK_API_ROOT = 'framework/java/android/app/appsearch'
+FRAMEWORK_API_ROOT = 'framework/java/external/android/app/appsearch'
 FRAMEWORK_API_TEST_ROOT = (
         '../../core/tests/coretests/src/'
         'android/app/appsearch/external')
@@ -52,18 +52,10 @@
         self._jetpack_appsearch_root = jetpack_appsearch_root
         self._framework_appsearch_root = framework_appsearch_root
 
-    def _PruneDir(self, dir_to_prune, allow_list=None):
-        all_files = []
+    def _PruneDir(self, dir_to_prune):
         for walk_path, walk_folders, walk_files in os.walk(dir_to_prune):
             for walk_filename in walk_files:
                 abs_path = os.path.join(walk_path, walk_filename)
-                all_files.append(abs_path)
-
-        for abs_path in all_files:
-            rel_path = os.path.relpath(abs_path, dir_to_prune)
-            if allow_list and rel_path in allow_list:
-                print('Prune: skip "%s"' % abs_path)
-            else:
                 print('Prune: remove "%s"' % abs_path)
                 os.remove(abs_path)
 
@@ -137,14 +129,7 @@
         api_test_dest_dir = os.path.join(self._framework_appsearch_root, FRAMEWORK_API_TEST_ROOT)
 
         # Prune existing files
-        self._PruneDir(api_dest_dir, allow_list=[
-            'AppSearchBatchResult.java',
-            'AppSearchManager.java',
-            'AppSearchManagerFrameworkInitializer.java',
-            'AppSearchResult.java',
-            'IAppSearchManager.aidl',
-            'SearchResults.java',
-        ])
+        self._PruneDir(api_dest_dir)
         self._PruneDir(api_test_dest_dir)
 
         # Copy api classes. We can't use _TransformAndCopyFolder here because we
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index d7217be..101356e 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -36,6 +36,7 @@
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
 import org.junit.Before;
@@ -45,9 +46,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 public class AppSearchImplTest {
     @Rule
@@ -69,7 +68,8 @@
                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
                                 .build())
                         .build();
-        mVisibilitySchemaProto = SchemaToProtoConverter.convert(visibilityAppSearchSchema);
+        mVisibilitySchemaProto =
+                SchemaToProtoConverter.toSchemaTypeConfigProto(visibilityAppSearchSchema);
     }
 
     /**
@@ -278,16 +278,16 @@
     @Test
     public void testOptimize() throws Exception {
         // Insert schema
-        Set<AppSearchSchema> schemas =
-                Collections.singleton(new AppSearchSchema.Builder("type").build());
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
         mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
 
         // Insert enough documents.
         for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
                 + AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
             GenericDocument document =
-                    new GenericDocument.Builder("uri" + i, "type").setNamespace(
+                    new GenericDocument.Builder<>("uri" + i, "type").setNamespace(
                             "namespace").build();
             mAppSearchImpl.putDocument("database", document);
         }
@@ -326,20 +326,19 @@
                 SearchSpecProto.newBuilder().setQuery("");
 
         // Insert schema
-        Set<AppSearchSchema> schemas =
-                Collections.singleton(new AppSearchSchema.Builder("type").build());
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
         mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
 
         // Insert document
-        GenericDocument document = new GenericDocument.Builder("uri", "type").setNamespace(
+        GenericDocument document = new GenericDocument.Builder<>("uri", "type").setNamespace(
                 "namespace").build();
         mAppSearchImpl.putDocument("database", document);
 
         // Rewrite SearchSpec
-        mAppSearchImpl.rewriteSearchSpecForDatabasesLocked(searchSpecProto,
-                Collections.singleton(
-                        "database"));
+        mAppSearchImpl.rewriteSearchSpecForDatabasesLocked(
+                searchSpecProto, Collections.singleton("database"));
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly("database/type");
         assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly("database/namespace");
     }
@@ -350,20 +349,20 @@
                 SearchSpecProto.newBuilder().setQuery("");
 
         // Insert schema
-        Set<AppSearchSchema> schemas = Set.of(
+        List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("typeA").build(),
                 new AppSearchSchema.Builder("typeB").build());
         mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
         mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
 
         // Insert documents
-        GenericDocument document1 = new GenericDocument.Builder("uri", "typeA").setNamespace(
+        GenericDocument document1 = new GenericDocument.Builder<>("uri", "typeA").setNamespace(
                 "namespace").build();
         mAppSearchImpl.putDocument("database1", document1);
 
-        GenericDocument document2 = new GenericDocument.Builder("uri", "typeB").setNamespace(
+        GenericDocument document2 = new GenericDocument.Builder<>("uri", "typeB").setNamespace(
                 "namespace").build();
         mAppSearchImpl.putDocument("database2", document2);
 
@@ -416,11 +415,11 @@
 
     @Test
     public void testSetSchema() throws Exception {
-        Set<AppSearchSchema> schemas =
-                Collections.singleton(new AppSearchSchema.Builder("Email").build());
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Email").build());
         // Set schema Email to AppSearch database1
         mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -436,19 +435,22 @@
 
     @Test
     public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception {
-        mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
                         "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singleton("schema1"), /*forceOverride=*/ false);
+                Collections.singletonList("schema1"), /*forceOverride=*/ false);
 
         // "schema1" is platform hidden now
         assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                 "database")).containsExactly("database/schema1");
 
         // Add a new schema, and include the already-existing "schema1"
-        mAppSearchImpl.setSchema("database", Set.of(new AppSearchSchema.Builder(
-                        "schema1").build(), new AppSearchSchema.Builder(
-                        "schema2").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singleton("schema1"), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema(
+                "database",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("schema1").build(),
+                        new AppSearchSchema.Builder("schema2").build()),
+                /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"),
+                /*forceOverride=*/ false);
 
         // Check that "schema1" is still platform hidden, but "schema2" is the default platform
         // visible.
@@ -458,12 +460,12 @@
 
     @Test
     public void testRemoveSchema() throws Exception {
-        Set<AppSearchSchema> schemas = new HashSet<>();
-        schemas.add(new AppSearchSchema.Builder("Email").build());
-        schemas.add(new AppSearchSchema.Builder("Document").build());
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("Email").build(),
+                new AppSearchSchema.Builder("Document").build());
         // Set schema Email and Document to AppSearch database1
         mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -478,19 +480,20 @@
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
 
-        final Set<AppSearchSchema> finalSchemas = Collections.singleton(new AppSearchSchema.Builder(
-                "Email").build());
+        final List<AppSearchSchema> finalSchemas = Collections.singletonList(
+                new AppSearchSchema.Builder(
+                        "Email").build());
         // Check the incompatible error has been thrown.
         AppSearchException e = assertThrows(AppSearchException.class, () ->
                 mAppSearchImpl.setSchema("database1",
                         finalSchemas, /*schemasNotPlatformSurfaceable=*/
-                        Collections.emptySet(), /*forceOverride=*/ false));
+                        Collections.emptyList(), /*forceOverride=*/ false));
         assertThat(e).hasMessageThat().contains("Schema is incompatible");
         assertThat(e).hasMessageThat().contains("Deleted types: [database1/Document]");
 
         // ForceOverride to delete.
         mAppSearchImpl.setSchema("database1", finalSchemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ true);
+                Collections.emptyList(), /*forceOverride=*/ true);
 
         // Check Document schema is removed.
         expectedProto = SchemaProto.newBuilder()
@@ -507,15 +510,15 @@
     @Test
     public void testRemoveSchema_differentDataBase() throws Exception {
         // Create schemas
-        Set<AppSearchSchema> schemas = new HashSet<>();
-        schemas.add(new AppSearchSchema.Builder("Email").build());
-        schemas.add(new AppSearchSchema.Builder("Document").build());
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("Email").build(),
+                new AppSearchSchema.Builder("Document").build());
 
         // Set schema Email and Document to AppSearch database1 and 2
         mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
         mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -533,9 +536,9 @@
                 .containsExactlyElementsIn(expectedTypes);
 
         // Save only Email to database1 this time.
-        schemas = Collections.singleton(new AppSearchSchema.Builder("Email").build());
+        schemas = Collections.singletonList(new AppSearchSchema.Builder("Email").build());
         mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ true);
+                Collections.emptyList(), /*forceOverride=*/ true);
 
         // Create expected schemaType list, database 1 should only contain Email but database 2
         // remains in same.
@@ -556,9 +559,9 @@
 
     @Test
     public void testRemoveSchema_removedFromVisibilityStore() throws Exception {
-        mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
                         "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singleton("schema1"), /*forceOverride=*/ false);
+                Collections.singletonList("schema1"), /*forceOverride=*/ false);
 
         // "schema1" is platform hidden now
         assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
@@ -566,8 +569,8 @@
 
         // Remove "schema1" by force overriding
         mAppSearchImpl.setSchema("database",
-                Collections.emptySet(), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ true);
+                Collections.emptyList(), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*forceOverride=*/ true);
 
         // Check that "schema1" is no longer considered platform hidden
         assertThat(
@@ -576,9 +579,9 @@
 
         // Add "schema1" back, it gets default visibility settings which means it's not platform
         // hidden.
-        mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
                         "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
         assertThat(
                 mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                         "database")).isEmpty();
@@ -586,9 +589,9 @@
 
     @Test
     public void testSetSchema_defaultPlatformVisible() throws Exception {
-        mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
                         "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
         assertThat(
                 mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                         "database")).isEmpty();
@@ -596,9 +599,9 @@
 
     @Test
     public void testSetSchema_platformHidden() throws Exception {
-        mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
                         "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singleton("Schema"), /*forceOverride=*/ false);
+                Collections.singletonList("Schema"), /*forceOverride=*/ false);
         assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                 "database")).containsExactly("database/Schema");
     }
@@ -608,9 +611,9 @@
         // Nothing exists yet
         assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isFalse();
 
-        mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
                         "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
         assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isTrue();
 
         assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "UnknownSchema")).isFalse();
@@ -623,16 +626,16 @@
                 VisibilityStore.DATABASE_NAME);
 
         // Has database1
-        mAppSearchImpl.setSchema("database1", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database1", Collections.singletonList(new AppSearchSchema.Builder(
                         "schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
         assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
                 VisibilityStore.DATABASE_NAME, "database1");
 
         // Has both databases
-        mAppSearchImpl.setSchema("database2", Collections.singleton(new AppSearchSchema.Builder(
+        mAppSearchImpl.setSchema("database2", Collections.singletonList(new AppSearchSchema.Builder(
                         "schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptySet(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*forceOverride=*/ false);
         assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
                 VisibilityStore.DATABASE_NAME, "database1", "database2");
     }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
index d4a30f4..f3732a1 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
@@ -18,13 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableSet;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
 import java.util.Collections;
-import java.util.Set;
 
 public class VisibilityStoreTest {
 
@@ -41,20 +42,28 @@
 
     @Test
     public void testSetVisibility() throws Exception {
-        mVisibilityStore.setVisibility(
-                "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema2"));
+        mVisibilityStore.setVisibility("database",
+                /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2"));
         assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
-                .containsExactly("schema1", "schema2");
+                .containsExactlyElementsIn(ImmutableSet.of("schema1", "schema2"));
 
         // New .setVisibility() call completely overrides previous visibility settings. So
-        // "schema1" isn't preserved.
-        mVisibilityStore.setVisibility(
-                "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema3"));
+        // "schema2" isn't preserved.
+        mVisibilityStore.setVisibility("database",
+                /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema3"));
         assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
-                .containsExactly("schema1", "schema3");
+                .containsExactlyElementsIn(ImmutableSet.of("schema1", "schema3"));
 
         mVisibilityStore.setVisibility(
                 "database", /*schemasNotPlatformSurfaceable=*/ Collections.emptySet());
         assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database")).isEmpty();
     }
+
+    @Test
+    public void testEmptyDatabase() throws Exception {
+        mVisibilityStore.setVisibility(LocalStorage.DEFAULT_DATABASE_NAME,
+                /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2"));
+        assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable(""))
+                .containsExactlyElementsIn(ImmutableSet.of("schema1", "schema2"));
+    }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index 4fa010f..1245262 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -84,21 +84,20 @@
                         .addBytesValues(ByteString.copyFrom(BYTE_ARRAY_1))
                         .addBytesValues(ByteString.copyFrom(BYTE_ARRAY_2)));
         propertyProtoMap.put("documentKey1",
-                PropertyProto.newBuilder().setName("documentKey1")
-                        .addDocumentValues(
-                                GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_1)));
+                PropertyProto.newBuilder().setName("documentKey1").addDocumentValues(
+                        GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_1)));
         propertyProtoMap.put("documentKey2",
-                PropertyProto.newBuilder().setName("documentKey2")
-                        .addDocumentValues(
-                                GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_2)));
+                PropertyProto.newBuilder().setName("documentKey2").addDocumentValues(
+                        GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_2)));
         List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
         Collections.sort(sortedKey);
         for (String key : sortedKey) {
             documentProtoBuilder.addProperties(propertyProtoMap.get(key));
         }
         DocumentProto documentProto = documentProtoBuilder.build();
-        assertThat(GenericDocumentToProtoConverter.convert(document))
+        assertThat(GenericDocumentToProtoConverter.toDocumentProto(document))
                 .isEqualTo(documentProto);
-        assertThat(document).isEqualTo(GenericDocumentToProtoConverter.convert(documentProto));
+        assertThat(document)
+                .isEqualTo(GenericDocumentToProtoConverter.toGenericDocument(documentProto));
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 0fb39db..889b0c7 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -69,7 +69,10 @@
                         )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.convert(emailSchema)).isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema))
+                .isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+                .isEqualTo(emailSchema);
     }
 
     @Test
@@ -113,7 +116,9 @@
                         )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.convert(musicRecordingSchema))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(musicRecordingSchema))
                 .isEqualTo(expectedMusicRecordingProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedMusicRecordingProto))
+                .isEqualTo(musicRecordingSchema);
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index d7ecf7b..9ffdeb9 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -77,7 +77,7 @@
 
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
         for (SearchResult result : searchResultPage.getResults()) {
             SearchResult.MatchInfo match = result.getMatches().get(0);
             assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
@@ -124,7 +124,7 @@
                 .build();
 
         SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
         for (SearchResult result : searchResultPage.getResults()) {
             assertThat(result.getMatches()).isEmpty();
         }
@@ -186,7 +186,7 @@
 
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
         for (SearchResult result : searchResultPage.getResults()) {
 
             SearchResult.MatchInfo match1 = result.getMatches().get(0);
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 2496cb4..9c25f4b 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -61,6 +61,7 @@
 import com.google.android.icing.proto.StatusProto;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -232,16 +233,19 @@
      *                                      which do not comply with the new schema will be deleted.
      * @throws AppSearchException on IcingSearchEngine error.
      */
-    public void setSchema(@NonNull String databaseName, @NonNull Set<AppSearchSchema> schemas,
-            @NonNull Set<String> schemasNotPlatformSurfaceable,
+    public void setSchema(
+            @NonNull String databaseName,
+            @NonNull List<AppSearchSchema> schemas,
+            @NonNull List<String> schemasNotPlatformSurfaceable,
             boolean forceOverride) throws AppSearchException {
         mReadWriteLock.writeLock().lock();
         try {
             SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
 
             SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
-            for (AppSearchSchema schema : schemas) {
-                SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema);
+            for (int i = 0; i < schemas.size(); i++) {
+                SchemaTypeConfigProto schemaTypeProto =
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(schemas.get(i));
                 newSchemaBuilder.addTypes(schemaTypeProto);
             }
 
@@ -280,8 +284,9 @@
             String databasePrefix = getDatabasePrefix(databaseName);
             Set<String> qualifiedSchemasNotPlatformSurfaceable =
                     new ArraySet<>(schemasNotPlatformSurfaceable.size());
-            for (String schema : schemasNotPlatformSurfaceable) {
-                qualifiedSchemasNotPlatformSurfaceable.add(databasePrefix + schema);
+            for (int i = 0; i < schemasNotPlatformSurfaceable.size(); i++) {
+                qualifiedSchemasNotPlatformSurfaceable.add(
+                        databasePrefix + schemasNotPlatformSurfaceable.get(i));
             }
             mVisibilityStoreLocked.setVisibility(databaseName,
                     qualifiedSchemasNotPlatformSurfaceable);
@@ -301,6 +306,56 @@
     }
 
     /**
+     * Retrieves the AppSearch schema for this database.
+     *
+     * <p>This method belongs to query group.
+     *
+     * @param databaseName The name of the database where this schema lives.
+     * @throws AppSearchException on IcingSearchEngine error.
+     */
+    @NonNull
+    public List<AppSearchSchema> getSchema(@NonNull String databaseName) throws AppSearchException {
+        SchemaProto fullSchema;
+        mReadWriteLock.readLock().lock();
+        try {
+            fullSchema = getSchemaProtoLocked();
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+
+        List<AppSearchSchema> result = new ArrayList<>();
+        for (int i = 0; i < fullSchema.getTypesCount(); i++) {
+            String typeDatabase = getDatabaseName(fullSchema.getTypes(i).getSchemaType());
+            if (!databaseName.equals(typeDatabase)) {
+                continue;
+            }
+            // Rewrite SchemaProto.types.schema_type
+            SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(i).toBuilder();
+            String newSchemaType =
+                    typeConfigBuilder.getSchemaType().substring(databaseName.length() + 1);
+            typeConfigBuilder.setSchemaType(newSchemaType);
+
+            // Rewrite SchemaProto.types.properties.schema_type
+            for (int propertyIdx = 0;
+                    propertyIdx < typeConfigBuilder.getPropertiesCount();
+                    propertyIdx++) {
+                PropertyConfigProto.Builder propertyConfigBuilder =
+                        typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+                if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
+                    String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
+                            .substring(databaseName.length() + 1);
+                    propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+                    typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+                }
+            }
+
+            AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder);
+            result.add(schema);
+        }
+        return result;
+    }
+
+    /**
      * Adds a document to the AppSearch index.
      *
      * <p>This method belongs to mutate group.
@@ -311,7 +366,7 @@
      */
     public void putDocument(@NonNull String databaseName, @NonNull GenericDocument document)
             throws AppSearchException {
-        DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.convert(
+        DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto(
                 document).toBuilder();
         addPrefixToDocument(documentBuilder, getDatabasePrefix(databaseName));
 
@@ -355,7 +410,7 @@
 
         DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
         removeDatabasesFromDocument(documentBuilder);
-        return GenericDocumentToProtoConverter.convert(documentBuilder.build());
+        return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build());
     }
 
     /**
@@ -937,7 +992,7 @@
                 resultsBuilder.setResults(i, resultBuilder);
             }
         }
-        return SearchResultToProtoConverter.convertToSearchResultPage(resultsBuilder);
+        return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder);
     }
 
     @GuardedBy("mReadWriteLock")
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 42223be..3426cfe 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -23,6 +23,7 @@
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSession;
@@ -41,12 +42,22 @@
  * An AppSearch storage system which stores data locally in the app's storage space using a bundled
  * version of the search native library.
  *
+ * <p>The search native library is an on-device searching library that allows apps to define
+ * {@link androidx.appsearch.app.AppSearchSchema}s, save and query a variety of
+ * {@link androidx.appsearch.annotation.AppSearchDocument}s. The library needs to be initialized
+ * before using, which will create a folder to save data in the app's storage space.
+ *
  * <p>Queries are executed multi-threaded, but a single thread is used for mutate requests (put,
  * delete, etc..).
  */
 public class LocalStorage {
-    /** The default empty database name.*/
-    private static final String DEFAULT_DATABASE_NAME = "";
+    /**
+     * The default empty database name.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @VisibleForTesting
+    public static final String DEFAULT_DATABASE_NAME = "";
 
     private static volatile ListenableFuture<AppSearchResult<LocalStorage>> sInstance;
 
@@ -82,7 +93,9 @@
             }
 
             /**
-             * Sets the name of the database to create or open.
+             * Sets the name of the database associated with {@link AppSearchSession}.
+             *
+             * <p>{@link AppSearchSession} will create or open a database under the given name.
              *
              * <p>Databases with different names are fully separate with distinct types, namespaces,
              * and data.
@@ -160,8 +173,11 @@
     /**
      * Opens a new {@link AppSearchSession} on this storage.
      *
-     * <p>If the system is not initialized, it will be initialized using the provided
-     * {@code context}.
+     * <p>This process requires a native search library. If it's not created, the initialization
+     * process will create one.
+     *
+     * @param context The {@link SearchContext} contains all information to create a new
+     *                {@link AppSearchSession}
      */
     @NonNull
     public static ListenableFuture<AppSearchResult<AppSearchSession>> createSearchSession(
@@ -182,8 +198,11 @@
     /**
      * Opens a new {@link GlobalSearchSession} on this storage.
      *
-     * <p>If the system is not initialized, it will be initialized using the provided
-     * {@code context}.
+     * <p>This process requires a native search library. If it's not created, the initialization
+     * process will create one.
+     *
+     * @param context The {@link GlobalSearchContext} contains all information to create a new
+     *                {@link GlobalSearchSession}
      * @hide
      */
     @NonNull
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 78fd1d4..f9440ec 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -21,6 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByUriRequest;
@@ -30,10 +31,14 @@
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.localstorage.util.FutureUtil;
+import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 
@@ -65,8 +70,10 @@
         return execute(() -> {
             try {
                 mAppSearchImpl.setSchema(
-                        mDatabaseName, request.getSchemas(),
-                        request.getSchemasNotPlatformSurfaceable(), request.isForceOverride());
+                        mDatabaseName,
+                        new ArrayList<>(request.getSchemas()),
+                        new ArrayList<>(request.getSchemasNotPlatformSurfaceable()),
+                        request.isForceOverride());
                 return AppSearchResult.newSuccessfulResult(/*value=*/ null);
             } catch (Throwable t) {
                 return throwableToFailedResult(t);
@@ -76,6 +83,19 @@
 
     @Override
     @NonNull
+    public ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema() {
+        return execute(() -> {
+            try {
+                List<AppSearchSchema> schemas = mAppSearchImpl.getSchema(mDatabaseName);
+                return AppSearchResult.newSuccessfulResult(new ArraySet<>(schemas));
+            } catch (Throwable t) {
+                return throwableToFailedResult(t);
+            }
+        });
+    }
+
+    @Override
+    @NonNull
     public ListenableFuture<AppSearchBatchResult<String, Void>> putDocuments(
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
index a95231f..935662e 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
@@ -65,7 +65,11 @@
     static final String DATABASE_NAME = "$$__AppSearch__Database";
 
     // Namespace of documents that contain visibility settings
-    private static final String NAMESPACE = "namespace";
+    private static final String NAMESPACE = GenericDocument.DEFAULT_NAMESPACE;
+
+    // Prefix to add to all visibility document uri's. IcingSearchEngine doesn't allow empty uri's.
+    private static final String URI_PREFIX = "uri:";
+
     private final AppSearchImpl mAppSearchImpl;
 
     // The map contains schemas that are platform-hidden for each database. All schemas in the map
@@ -97,7 +101,7 @@
         if (!mAppSearchImpl.hasSchemaTypeLocked(DATABASE_NAME, SCHEMA_TYPE)) {
             // Schema type doesn't exist yet. Add it.
             mAppSearchImpl.setSchema(DATABASE_NAME,
-                    Collections.singleton(new AppSearchSchema.Builder(SCHEMA_TYPE)
+                    Collections.singletonList(new AppSearchSchema.Builder(SCHEMA_TYPE)
                             .addProperty(new AppSearchSchema.PropertyConfig.Builder(
                                     NOT_PLATFORM_SURFACEABLE_PROPERTY)
                                     .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
@@ -105,7 +109,7 @@
                                             AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
                                     .build())
                             .build()),
-                    /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
+                    /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
                     /*forceOverride=*/ false);
         }
 
@@ -119,11 +123,12 @@
             try {
                 // Note: We use the other clients' database names as uris
                 GenericDocument document = mAppSearchImpl.getDocument(
-                        DATABASE_NAME, NAMESPACE, /*uri=*/ database);
+                        DATABASE_NAME, NAMESPACE, /*uri=*/ addUriPrefix(database));
 
                 String[] schemas = document.getPropertyStringArray(
                         NOT_PLATFORM_SURFACEABLE_PROPERTY);
-                mNotPlatformSurfaceableMap.put(database, new ArraySet<>(Arrays.asList(schemas)));
+                mNotPlatformSurfaceableMap.put(database,
+                        new ArraySet<>(Arrays.asList(schemas)));
             } catch (AppSearchException e) {
                 if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
                     // TODO(b/172068212): This indicates some desync error. We were expecting a
@@ -155,7 +160,7 @@
 
         // Persist the document
         GenericDocument.Builder visibilityDocument = new GenericDocument.Builder(
-                /*uri=*/ databaseName, SCHEMA_TYPE)
+                /*uri=*/ addUriPrefix(databaseName), SCHEMA_TYPE)
                 .setNamespace(NAMESPACE);
         if (!schemasNotPlatformSurfaceable.isEmpty()) {
             visibilityDocument.setPropertyString(NOT_PLATFORM_SURFACEABLE_PROPERTY,
@@ -195,4 +200,14 @@
         mNotPlatformSurfaceableMap.clear();
         initialize();
     }
+
+    /**
+     * Adds a uri prefix to create a visibility store document's uri.
+     *
+     * @param uri Non-prefixed uri
+     * @return Prefixed uri
+     */
+    private static String addUriPrefix(String uri) {
+        return URI_PREFIX + uri;
+    }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index 2f4c23d..c27cb92 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -39,7 +39,7 @@
     /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */
     @NonNull
     @SuppressWarnings("unchecked")
-    public static DocumentProto convert(@NonNull GenericDocument document) {
+    public static DocumentProto toDocumentProto(@NonNull GenericDocument document) {
         Preconditions.checkNotNull(document);
         DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
         mProtoBuilder.setUri(document.getUri())
@@ -81,7 +81,7 @@
                 }
             } else if (documentValues != null) {
                 for (int j = 0; j < documentValues.length; j++) {
-                    DocumentProto proto = convert(documentValues[j]);
+                    DocumentProto proto = toDocumentProto(documentValues[j]);
                     propertyProto.addDocumentValues(proto);
                 }
             } else {
@@ -95,7 +95,7 @@
 
     /** Converts a {@link DocumentProto} into a {@link GenericDocument}. */
     @NonNull
-    public static GenericDocument convert(@NonNull DocumentProto proto) {
+    public static GenericDocument toGenericDocument(@NonNull DocumentProto proto) {
         Preconditions.checkNotNull(proto);
         GenericDocument.Builder<?> documentBuilder =
                 new GenericDocument.Builder<>(proto.getUri(), proto.getSchema())
@@ -140,7 +140,7 @@
             } else if (property.getDocumentValuesCount() > 0) {
                 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
                 for (int j = 0; j < values.length; j++) {
-                    values[j] = convert(property.getDocumentValues(j));
+                    values[j] = toGenericDocument(property.getDocumentValues(j));
                 }
                 documentBuilder.setPropertyDocument(name, values);
             } else {
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index 3d79150..f52c220 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.localstorage.converter;
 
+import android.util.Log;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
@@ -23,6 +25,7 @@
 
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.SchemaTypeConfigProtoOrBuilder;
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
 
@@ -34,6 +37,8 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SchemaToProtoConverter {
+    private static final String TAG = "AppSearchSchemaToProtoC";
+
     private SchemaToProtoConverter() {}
 
     /**
@@ -41,23 +46,23 @@
      * {@link SchemaTypeConfigProto}.
      */
     @NonNull
-    public static SchemaTypeConfigProto convert(@NonNull AppSearchSchema schema) {
+    public static SchemaTypeConfigProto toSchemaTypeConfigProto(@NonNull AppSearchSchema schema) {
         Preconditions.checkNotNull(schema);
         SchemaTypeConfigProto.Builder protoBuilder =
                 SchemaTypeConfigProto.newBuilder().setSchemaType(schema.getSchemaType());
         List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
-            PropertyConfigProto propertyProto = convertProperty(properties.get(i));
+            PropertyConfigProto propertyProto = toPropertyConfigProto(properties.get(i));
             protoBuilder.addProperties(propertyProto);
         }
         return protoBuilder.build();
     }
 
     @NonNull
-    private static PropertyConfigProto convertProperty(
+    private static PropertyConfigProto toPropertyConfigProto(
             @NonNull AppSearchSchema.PropertyConfig property) {
         Preconditions.checkNotNull(property);
-        PropertyConfigProto.Builder propertyConfigProto = PropertyConfigProto.newBuilder()
+        PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
                 .setPropertyName(property.getName());
         StringIndexingConfig.Builder indexingConfig = StringIndexingConfig.newBuilder();
 
@@ -68,12 +73,12 @@
         if (dataTypeProto == null) {
             throw new IllegalArgumentException("Invalid dataType: " + dataType);
         }
-        propertyConfigProto.setDataType(dataTypeProto);
+        builder.setDataType(dataTypeProto);
 
         // Set schemaType
         String schemaType = property.getSchemaType();
         if (schemaType != null) {
-            propertyConfigProto.setSchemaType(schemaType);
+            builder.setSchemaType(schemaType);
         }
 
         // Set cardinality
@@ -83,7 +88,7 @@
         if (cardinalityProto == null) {
             throw new IllegalArgumentException("Invalid cardinality: " + dataType);
         }
-        propertyConfigProto.setCardinality(cardinalityProto);
+        builder.setCardinality(cardinalityProto);
 
         // Set indexingType
         @AppSearchSchema.PropertyConfig.IndexingType int indexingType = property.getIndexingType();
@@ -114,7 +119,63 @@
         indexingConfig.setTokenizerType(tokenizerTypeProto);
 
         // Build!
-        propertyConfigProto.setStringIndexingConfig(indexingConfig);
-        return propertyConfigProto.build();
+        builder.setStringIndexingConfig(indexingConfig);
+        return builder.build();
+    }
+
+    /**
+     * Converts a {@link SchemaTypeConfigProto} into an
+     * {@link androidx.appsearch.app.AppSearchSchema}.
+     */
+    @NonNull
+    public static AppSearchSchema toAppSearchSchema(@NonNull SchemaTypeConfigProtoOrBuilder proto) {
+        Preconditions.checkNotNull(proto);
+        AppSearchSchema.Builder builder = new AppSearchSchema.Builder(proto.getSchemaType());
+        List<PropertyConfigProto> properties = proto.getPropertiesList();
+        for (int i = 0; i < properties.size(); i++) {
+            AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
+            builder.addProperty(propertyConfig);
+        }
+        return builder.build();
+    }
+
+    @NonNull
+    private static AppSearchSchema.PropertyConfig toPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        Preconditions.checkNotNull(proto);
+        AppSearchSchema.PropertyConfig.Builder builder =
+                new AppSearchSchema.PropertyConfig.Builder(proto.getPropertyName())
+                        .setDataType(proto.getDataType().getNumber())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .setTokenizerType(
+                                proto.getStringIndexingConfig().getTokenizerType().getNumber());
+
+        // Set schema
+        if (!proto.getSchemaType().isEmpty()) {
+            builder.setSchemaType(proto.getSchemaType());
+        }
+
+        // Set indexingType
+        @AppSearchSchema.PropertyConfig.IndexingType int indexingType;
+        TermMatchType.Code termMatchTypeProto = proto.getStringIndexingConfig().getTermMatchType();
+        switch (termMatchTypeProto) {
+            case UNKNOWN:
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+                break;
+            case EXACT_ONLY:
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS;
+                break;
+            case PREFIX:
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
+                break;
+            default:
+                // Avoid crashing in the 'read' path; we should try to interpret the document to the
+                // extent possible.
+                Log.w(TAG, "Invalid indexingType: " + termMatchTypeProto.getNumber());
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+        }
+        builder.setIndexingType(indexingType);
+
+        return builder.build();
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index 97c4adc1..8d7257c 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -42,13 +42,12 @@
 
     /** Translate a {@link SearchResultProto} into {@link SearchResultPage}. */
     @NonNull
-    public static SearchResultPage convertToSearchResultPage(
-            @NonNull SearchResultProtoOrBuilder proto) {
+    public static SearchResultPage toSearchResultPage(@NonNull SearchResultProtoOrBuilder proto) {
         Bundle bundle = new Bundle();
         bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
         ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
-            resultBundles.add(convertToSearchResultBundle(proto.getResults(i)));
+            resultBundles.add(toSearchResultBundle(proto.getResults(i)));
         }
         bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
         return new SearchResultPage(bundle);
@@ -56,10 +55,11 @@
 
     /** Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. */
     @NonNull
-    private static Bundle convertToSearchResultBundle(
+    private static Bundle toSearchResultBundle(
             @NonNull SearchResultProto.ResultProtoOrBuilder proto) {
         Bundle bundle = new Bundle();
-        GenericDocument document = GenericDocumentToProtoConverter.convert(proto.getDocument());
+        GenericDocument document =
+                GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument());
         bundle.putBundle(SearchResult.DOCUMENT_FIELD, document.getBundle());
 
         ArrayList<Bundle> matchList = new ArrayList<>();
diff --git a/benchmark/common/api/current.txt b/benchmark/common/api/current.txt
index c023743..167ebf4 100644
--- a/benchmark/common/api/current.txt
+++ b/benchmark/common/api/current.txt
@@ -4,6 +4,9 @@
   public final class ArgumentsKt {
   }
 
+  public final class BenchmarkResultKt {
+  }
+
   public final class BenchmarkState {
     method public boolean keepRunning();
     method public void pauseTiming();
diff --git a/benchmark/common/api/public_plus_experimental_current.txt b/benchmark/common/api/public_plus_experimental_current.txt
index dc2f41d..e54d1d6 100644
--- a/benchmark/common/api/public_plus_experimental_current.txt
+++ b/benchmark/common/api/public_plus_experimental_current.txt
@@ -4,6 +4,34 @@
   public final class ArgumentsKt {
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class BenchmarkResult {
+    ctor public BenchmarkResult(String className, String testName, long totalRunTimeNs, java.util.List<androidx.benchmark.MetricResult> metrics, int repeatIterations, long thermalThrottleSleepSeconds, int warmupIterations);
+    method public String component1();
+    method public String component2();
+    method public long component3();
+    method public java.util.List<androidx.benchmark.MetricResult> component4();
+    method public int component5();
+    method public long component6();
+    method public int component7();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.benchmark.BenchmarkResult copy(String className, String testName, long totalRunTimeNs, java.util.List<androidx.benchmark.MetricResult> metrics, int repeatIterations, long thermalThrottleSleepSeconds, int warmupIterations);
+    method public String getClassName();
+    method public java.util.List<androidx.benchmark.MetricResult> getMetrics();
+    method public int getRepeatIterations();
+    method public androidx.benchmark.Stats getStats(String which);
+    method public String getTestName();
+    method public int getWarmupIterations();
+    property public final String className;
+    property public final java.util.List<androidx.benchmark.MetricResult> metrics;
+    property public final int repeatIterations;
+    property public final String testName;
+    property public final int warmupIterations;
+    field public final long thermalThrottleSleepSeconds;
+    field public final long totalRunTimeNs;
+  }
+
+  public final class BenchmarkResultKt {
+  }
+
   public final class BenchmarkState {
     ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public BenchmarkState();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public long getMinTimeNanos();
@@ -40,9 +68,26 @@
   public final class MetricNameUtilsKt {
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class MetricResult {
+    ctor public MetricResult(java.util.List<java.lang.Long> data, androidx.benchmark.Stats stats);
+    ctor public MetricResult(String name, long[] data);
+    method public java.util.List<java.lang.Long> component1();
+    method public androidx.benchmark.Stats component2();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.benchmark.MetricResult copy(java.util.List<java.lang.Long> data, androidx.benchmark.Stats stats);
+    method public java.util.List<java.lang.Long> getData();
+    method public androidx.benchmark.Stats getStats();
+    property public final java.util.List<java.lang.Long> data;
+    property public final androidx.benchmark.Stats stats;
+  }
+
   public final class ProfilerKt {
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class ResultWriter {
+    method public void appendReport(androidx.benchmark.BenchmarkResult benchmarkResult);
+    field public static final androidx.benchmark.ResultWriter INSTANCE;
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class Stats {
     ctor public Stats(long[] data, String name);
     method public long getMax();
diff --git a/benchmark/common/api/restricted_current.txt b/benchmark/common/api/restricted_current.txt
index 84aef03..b59d142 100644
--- a/benchmark/common/api/restricted_current.txt
+++ b/benchmark/common/api/restricted_current.txt
@@ -4,6 +4,9 @@
   public final class ArgumentsKt {
   }
 
+  public final class BenchmarkResultKt {
+  }
+
   public final class BenchmarkState {
     method public boolean keepRunning();
     method @kotlin.PublishedApi internal boolean keepRunningInternal();
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
index f65209e..ef9b83d 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
@@ -262,12 +262,16 @@
             thermalThrottleSleepSeconds = 0,
             repeatIterations = 1
         )
-        val expectedReport = BenchmarkState.Report(
+        val expectedReport = BenchmarkResult(
             className = "className",
             testName = "testName",
             totalRunTimeNs = 900000000,
-            data = listOf(listOf(100L, 200L, 300L)),
-            stats = listOf(Stats(longArrayOf(100, 200, 300), "timeNs")),
+            metrics = listOf(
+                MetricResult(
+                    name = "timeNs",
+                    data = longArrayOf(100, 200, 300)
+                )
+            ),
             repeatIterations = 1,
             thermalThrottleSleepSeconds = 0,
             warmupIterations = 1
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
index cc7513a..cae9a18 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
@@ -28,30 +28,31 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ResultWriterTest {
-
+public class ResultWriterTest {
     @get:Rule
     val tempFolder = TemporaryFolder()
 
-    private val data = arrayOf(longArrayOf(100, 101, 102))
-    private val names = listOf("timeNs")
+    private val metricResults = listOf(
+        MetricResult(
+            name = "timeNs",
+            data = longArrayOf(100L, 101L, 102L)
+        )
+    )
 
-    private val reportA = BenchmarkState.Report(
+    private val reportA = BenchmarkResult(
         testName = "MethodA",
         className = "package.Class1",
         totalRunTimeNs = 900000000,
-        data = data.map { it.toList() },
-        stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+        metrics = metricResults,
         repeatIterations = 100000,
         thermalThrottleSleepSeconds = 90000000,
         warmupIterations = 8000
     )
-    private val reportB = BenchmarkState.Report(
+    private val reportB = BenchmarkResult(
         testName = "MethodB",
         className = "package.Class2",
         totalRunTimeNs = 900000000,
-        data = data.map { it.toList() },
-        stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+        metrics = metricResults,
         repeatIterations = 100000,
         thermalThrottleSleepSeconds = 90000000,
         warmupIterations = 8000
@@ -145,12 +146,11 @@
 
     @Test
     fun validateJsonWithParams() {
-        val reportWithParams = BenchmarkState.Report(
+        val reportWithParams = BenchmarkResult(
             testName = "MethodWithParams[number=2,primeNumber=true]",
             className = "package.Class",
             totalRunTimeNs = 900000000,
-            data = data.map { it.toList() },
-            stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+            metrics = metricResults,
             repeatIterations = 100000,
             thermalThrottleSleepSeconds = 90000000,
             warmupIterations = 8000
@@ -175,12 +175,11 @@
 
     @Test
     fun validateJsonWithInvalidParams() {
-        val reportWithInvalidParams = BenchmarkState.Report(
+        val reportWithInvalidParams = BenchmarkResult(
             testName = "MethodWithParams[number=2,=true,]",
             className = "package.Class",
             totalRunTimeNs = 900000000,
-            data = data.map { it.toList() },
-            stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+            metrics = metricResults,
             repeatIterations = 100000,
             thermalThrottleSleepSeconds = 90000000,
             warmupIterations = 8000
diff --git a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkResult.kt b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkResult.kt
new file mode 100644
index 0000000..7129b3c
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkResult.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Data for a single metric from a single benchmark test method run.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public data class MetricResult(
+    val data: List<Long>,
+    val stats: Stats
+) {
+    public constructor(
+        name: String,
+        data: LongArray
+    ) : this(data.toList(), Stats(data, name))
+}
+
+/**
+ * Data capture from a single benchmark test method run.
+ *
+ * Each field directly corresponds to JSON output, though not every JSON object may be
+ * represented directly here.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public data class BenchmarkResult(
+    val className: String,
+    val testName: String,
+    @JvmField // Suppress API lint (using JvmField instead of @Suppress to workaround b/175063229))
+    val totalRunTimeNs: Long,
+    val metrics: List<MetricResult>,
+    val repeatIterations: Int,
+    @JvmField // Suppress API lint (using JvmField instead of @Suppress to workaround b/175063229))
+    val thermalThrottleSleepSeconds: Long,
+    val warmupIterations: Int
+) {
+    public fun getStats(which: String): Stats {
+        return metrics.first { it.stats.name == which }.stats
+    }
+}
+
+internal fun metricResultList(stats: List<Stats>, data: List<LongArray>): List<MetricResult> {
+    require(stats.size == data.size)
+    return stats.mapIndexed { index, currentStats ->
+        MetricResult(data[index].toList(), currentStats)
+    }
+}
\ No newline at end of file
diff --git a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
index c74b069..6b66d0a 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
@@ -445,27 +445,11 @@
             " Call BenchmarkState.resumeTiming() before BenchmarkState.keepRunning()."
     }
 
-    internal data class Report(
-        val className: String,
-        val testName: String,
-        val totalRunTimeNs: Long,
-        val data: List<List<Long>>,
-        val stats: List<Stats>,
-        val repeatIterations: Int,
-        val thermalThrottleSleepSeconds: Long,
-        val warmupIterations: Int
-    ) {
-        fun getStats(which: String): Stats {
-            return stats.first { it.name == which }
-        }
-    }
-
-    private fun getReport(testName: String, className: String) = Report(
+    private fun getReport(testName: String, className: String) = BenchmarkResult(
         className = className,
         testName = testName,
         totalRunTimeNs = totalRunTimeNs,
-        data = allData.map { it.toList() },
-        stats = stats,
+        metrics = metricResultList(stats, allData),
         repeatIterations = iterationsPerRepeat,
         thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
         warmupIterations = warmupRepeats
@@ -609,12 +593,14 @@
         ) {
             val metricsContainer = MetricsContainer(REPEAT_COUNT = dataNs.size)
             metricsContainer.data[metricsContainer.data.lastIndex] = dataNs.toLongArray()
-            val report = Report(
+            val report = BenchmarkResult(
                 className = className,
                 testName = testName,
                 totalRunTimeNs = totalRunTimeNs,
-                data = metricsContainer.data.map { it.toList() },
-                stats = metricsContainer.captureFinished(maxIterations = 1),
+                metrics = metricResultList(
+                    stats = metricsContainer.captureFinished(maxIterations = 1),
+                    data = metricsContainer.data.toList()
+                ),
                 repeatIterations = repeatIterations,
                 thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
                 warmupIterations = warmupIterations
diff --git a/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt b/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
index c62a72fd..7f0219c 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
@@ -19,17 +19,20 @@
 import android.os.Build
 import android.util.JsonWriter
 import android.util.Log
+import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.test.platform.app.InstrumentationRegistry
 import java.io.File
 import java.io.IOException
 
-internal object ResultWriter {
-    @VisibleForTesting
-    internal val reports = ArrayList<BenchmarkState.Report>()
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public object ResultWriter {
 
-    fun appendReport(report: BenchmarkState.Report) {
-        reports.add(report)
+    @VisibleForTesting
+    internal val reports = ArrayList<BenchmarkResult>()
+
+    public fun appendReport(benchmarkResult: BenchmarkResult) {
+        reports.add(benchmarkResult)
 
         if (Arguments.outputEnable) {
             // Currently, we just overwrite the whole file
@@ -59,7 +62,7 @@
     }
 
     @VisibleForTesting
-    internal fun writeReport(file: File, reports: List<BenchmarkState.Report>) {
+    internal fun writeReport(file: File, benchmarkResults: List<BenchmarkResult>) {
         file.run {
             if (!exists()) {
                 parentFile?.mkdirs()
@@ -97,7 +100,7 @@
             writer.endObject()
 
             writer.name("benchmarks").beginArray()
-            reports.forEach { writer.reportObject(it) }
+            benchmarkResults.forEach { writer.reportObject(it) }
             writer.endArray()
 
             writer.endObject()
@@ -116,16 +119,16 @@
         return endObject()
     }
 
-    private fun JsonWriter.reportObject(report: BenchmarkState.Report): JsonWriter {
+    private fun JsonWriter.reportObject(benchmarkResult: BenchmarkResult): JsonWriter {
         beginObject()
-            .name("name").value(report.testName)
-            .name("params").paramsObject(report)
-            .name("className").value(report.className)
-            .name("totalRunTimeNs").value(report.totalRunTimeNs)
-            .name("metrics").metricsContainerObject(report.stats, report.data)
-            .name("warmupIterations").value(report.warmupIterations)
-            .name("repeatIterations").value(report.repeatIterations)
-            .name("thermalThrottleSleepSeconds").value(report.thermalThrottleSleepSeconds)
+            .name("name").value(benchmarkResult.testName)
+            .name("params").paramsObject(benchmarkResult)
+            .name("className").value(benchmarkResult.className)
+            .name("totalRunTimeNs").value(benchmarkResult.totalRunTimeNs)
+            .name("metrics").metricsContainerObject(benchmarkResult.metrics)
+            .name("warmupIterations").value(benchmarkResult.warmupIterations)
+            .name("repeatIterations").value(benchmarkResult.repeatIterations)
+            .name("thermalThrottleSleepSeconds").value(benchmarkResult.thermalThrottleSleepSeconds)
         return endObject()
     }
 
@@ -139,24 +142,23 @@
     }
 
     private fun JsonWriter.metricsContainerObject(
-        stats: List<Stats>,
-        data: List<List<Long>>
+        metricResults: List<MetricResult>
     ): JsonWriter {
         beginObject()
-        for (i in 0..stats.lastIndex) {
-            name(stats[i].name).beginObject()
-            statsObject(stats[i])
+        metricResults.forEach {
+            name(it.stats.name).beginObject()
+            statsObject(it.stats)
             name("runs").beginArray()
-            data[i].forEach { value(it) }
+            it.data.forEach { value(it) }
             endArray()
             endObject()
         }
         return endObject()
     }
 
-    private fun JsonWriter.paramsObject(report: BenchmarkState.Report): JsonWriter {
+    private fun JsonWriter.paramsObject(benchmarkResult: BenchmarkResult): JsonWriter {
         beginObject()
-        getParams(report.testName).forEach { name(it.key).value(it.value) }
+        getParams(benchmarkResult.testName).forEach { name(it.key).value(it.value) }
         return endObject()
     }
 
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
index df929f4..b33734b 100755
--- a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
@@ -94,6 +94,19 @@
     fi
 }
 
+# Disable CPU hotpluging by killing mpdecision service via ctl.stop system property.
+# This helper checks the state and existence of the mpdecision service via init.svc.
+# Possible values from init.svc: "stopped", "stopping", "running", "restarting"
+function_stop_mpdecision() {
+    MPDECISION_STATUS=`getprop init.svc.mpdecision`
+    while [ "$MPDECISION_STATUS" == "running" ] || [ "$MPDECISION_STATUS" == "restarting" ]; do
+        setprop ctl.stop mpdecision
+        # Give initrc some time to kill the mpdecision service.
+        sleep 0.1
+        MPDECISION_STATUS=`getprop init.svc.mpdecision`
+    done
+}
+
 # Find the min or max (little vs big) of CPU max frequency, and lock cores of the selected type to
 # an available frequency that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores.
 function_lock_cpu() {
@@ -111,10 +124,14 @@
     disableIndices=''
     cpu=0
 
+    # Stop mpdecision (CPU hotplug service) if it exists. Not available on all devices.
+    function_stop_mpdecision
+
     # Loop through all available cores; We have to check by the parent folder
     # "cpu#" instead of cpu#/online or cpu#/cpufreq directly, since they may
     # not be accessible yet.
     while [ -d ${CPU_BASE}/cpu${cpu} ]; do
+
         # Try to enable core, so we can find its frequencies.
         # Note: In cases where the online file is inaccessible, it represents a
         # core which cannot be turned off, so we simply assume it is enabled if
diff --git a/benchmark/integration-tests/macrobenchmark-target/build.gradle b/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 1e72999..4de1d5d 100644
--- a/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -22,8 +22,18 @@
     id("kotlin-android")
 }
 
+android {
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
+        }
+    }
+}
+
 dependencies {
-    api(KOTLIN_STDLIB)
+    implementation(KOTLIN_STDLIB)
     implementation(CONSTRAINT_LAYOUT, { transitive = true })
     implementation("androidx.arch.core:core-runtime:2.1.0")
     implementation("androidx.appcompat:appcompat:1.2.0")
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 44a22a8..58d1f37 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -13,25 +13,33 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="androidx.benchmark.integration.macrobenchmark.target">
 
     <application
+        android:label="Jetpack Benchmark Macrobenchmark Target"
         android:allowBackup="false"
         android:supportsRtl="true"
-        android:theme="@style/Theme.AppCompat">
+        android:theme="@style/Theme.AppCompat"
+        tools:ignore="MissingApplicationIcon">
 
         <!--
-        The activity needs to be exported so the Macro Benchmark Sample can discover activities
+        Activities need to be exported so the macrobenchmark can discover them
         under the new package visibility changes for Android 11.
          -->
         <activity
-            android:name=".MainActivity"
+            android:name=".TrivialStartupActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
         </activity>
 
         <activity
@@ -42,6 +50,5 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
     </application>
 </manifest>
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
index efddddb..f5d6a2a 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
@@ -24,16 +24,20 @@
 class RecyclerViewActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+        title = "RecyclerView Sample"
         setContentView(R.layout.activity_recycler_view)
         val recycler = findViewById<RecyclerView>(R.id.recycler)
-        val adapter = EntryAdapter(entries(1000))
+        val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 1000)
+        val adapter = EntryAdapter(entries(itemCount))
         recycler.layoutManager = LinearLayoutManager(this)
         recycler.adapter = adapter
     }
 
-    private fun entries(size: Int): List<Entry> {
-        return IntRange(0, size).map {
-            Entry("Item $it")
-        }
+    private fun entries(size: Int) = List(size) {
+        Entry("Item $it")
+    }
+
+    companion object {
+        const val EXTRA_ITEM_COUNT = "ITEM_COUNT"
     }
 }
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
similarity index 69%
rename from benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt
rename to benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
index 05177a9..52792d1 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
@@ -16,32 +16,16 @@
 
 package androidx.benchmark.integration.macrobenchmark.target
 
-import android.app.Activity
-import android.os.Build
 import android.os.Bundle
 import android.widget.TextView
-import androidx.annotation.RequiresApi
 import androidx.appcompat.app.AppCompatActivity
 
-class MainActivity : AppCompatActivity() {
+class TrivialStartupActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
 
         val notice = findViewById<TextView>(R.id.txtNotice)
         notice.setText(R.string.app_notice)
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-            KitkatActivityCompat.reportFullyDrawn(this)
-        }
-    }
-}
-
-/**
- * Wrapper avoids UnsafeApiCall lint
- */
-@RequiresApi(19)
-object KitkatActivityCompat {
-    fun reportFullyDrawn(activity: Activity) {
-        activity.reportFullyDrawn()
     }
 }
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
index d8d6754..7e77ae3 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
@@ -13,25 +13,32 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.cardview.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/card"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
-
-    <androidx.cardview.widget.CardView
-        android:id="@+id/card"
+    android:layout_height="wrap_content"
+    android:layout_margin="8dp">
+    <androidx.appcompat.widget.LinearLayoutCompat
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:padding="8dp">
+        android:orientation="horizontal">
 
-        <TextView
+        <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/content"
-            android:padding="8dp"
-            android:layout_width="match_parent"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_margin="16dp"
             tools:text="Sample text" />
+        <Space
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" />
 
-    </androidx.cardview.widget.CardView>
-
-</LinearLayout>
\ No newline at end of file
+        <androidx.appcompat.widget.AppCompatCheckBox
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp" />
+    </androidx.appcompat.widget.LinearLayoutCompat>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/build.gradle b/benchmark/integration-tests/macrobenchmark/build.gradle
index 3384fe8..58fb8e8 100644
--- a/benchmark/integration-tests/macrobenchmark/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -30,6 +30,7 @@
 android {
     defaultConfig {
         minSdkVersion 28
+        testInstrumentationRunnerArgument 'androidx.benchmark.output.enable', 'true'
     }
 }
 
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
index 2f8615f..6dd48d3 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
@@ -28,4 +28,5 @@
     <queries>
         <package android:name="androidx.benchmark.integration.macrobenchmark.target" />
     </queries>
+    <application android:requestLegacyExternalStorage="true"/>
 </manifest>
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
index ab8ba81..8d2e9a2 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
@@ -37,10 +37,8 @@
 @SdkSuppress(minSdkVersion = 29)
 @RunWith(Parameterized::class)
 class JankMetricValidation(
-    private val compilationMode: CompilationMode,
-    private val killProcess: Boolean
+    private val compilationMode: CompilationMode
 ) {
-
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
@@ -58,7 +56,6 @@
             packageName = PACKAGE_NAME,
             metrics = listOf(JankMetric()),
             compilationMode = compilationMode,
-            killProcessEachIteration = killProcess,
             iterations = 10
         )
 
@@ -67,15 +64,16 @@
             setupBlock = {
                 val intent = Intent()
                 intent.action = ACTION
-                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                 launchIntentAndWait(intent)
             }
         ) {
             val recycler = device.findObject(By.res(PACKAGE_NAME, RESOURCE_ID))
             // Setting a gesture margin is important otherwise gesture nav is triggered.
             recycler.setGestureMargin(device.displayWidth / 5)
-            recycler.fling(Direction.DOWN)
-            device.waitForIdle()
+            for (i in 1..10) {
+                recycler.scroll(Direction.DOWN, 2f)
+                device.waitForIdle()
+            }
         }
     }
 
@@ -85,17 +83,13 @@
             "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
         private const val RESOURCE_ID = "recycler"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+        @Parameterized.Parameters(name = "compilation_mode={0}")
         @JvmStatic
         fun jankParameters(): List<Array<Any>> {
-            val compilationModes = listOf(
+            return listOf(
                 CompilationMode.None,
                 CompilationMode.SpeedProfile(warmupIterations = 3)
-            )
-            val processKillOptions = listOf(false, false)
-            return compilationModes.zip(processKillOptions).map {
-                arrayOf(it.first, it.second)
-            }
+            ).map { arrayOf(it) }
         }
     }
 }
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt
deleted file mode 100644
index 908c5cc..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.integration.macrobenchmark
-
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.MacrobenchmarkConfig
-import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(AndroidJUnit4::class)
-class MacrobenchmarkRuleTest {
-    @get:Rule
-    val benchmarkRule = MacrobenchmarkRule()
-
-    @Test
-    fun basicTest() = benchmarkRule.measureRepeated(
-        MacrobenchmarkConfig(
-            packageName = "androidx.benchmark.integration.macrobenchmark.target",
-            listOf(StartupTimingMetric()),
-            CompilationMode.Speed,
-            killProcessEachIteration = true,
-            iterations = 4
-        )
-    ) {
-        pressHome()
-        launchPackageAndWait()
-    }
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
index 8bb9215..bd1951a 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
@@ -16,13 +16,14 @@
 
 package androidx.benchmark.integration.macrobenchmark
 
-import android.content.Intent
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.macrobenchmark
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -32,42 +33,36 @@
 @RunWith(Parameterized::class)
 class ProcessSpeedProfileValidation(
     private val compilationMode: CompilationMode,
-    private val killProcess: Boolean
+    private val startupMode: StartupMode
 ) {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
     @Test
-    fun start() {
-        val benchmarkName = "speed_profile_process_validation"
-        val config = MacrobenchmarkConfig(
+    fun start() = benchmarkRule.measureStartupRepeated(
+        MacrobenchmarkConfig(
             packageName = PACKAGE_NAME,
             metrics = listOf(StartupTimingMetric()),
             compilationMode = compilationMode,
-            killProcessEachIteration = killProcess,
-            iterations = 10
-        )
-        macrobenchmark(
-            benchmarkName = benchmarkName,
-            config = config
-        ) {
-            pressHome()
-            launchPackageAndWait { launchIntent ->
-                // Clear out any previous instances
-                launchIntent.flags =
-                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-            }
-        }
+            iterations = 3
+        ),
+        startupMode
+    ) {
+        pressHome()
+        launchPackageAndWait()
     }
 
     companion object {
         private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+        @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
         @JvmStatic
         fun kilProcessParameters(): List<Array<Any>> {
             val compilationModes = listOf(
                 CompilationMode.None,
                 CompilationMode.SpeedProfile(warmupIterations = 3)
             )
-            val processKillOptions = listOf(true, false)
+            val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
             return compilationModes.zip(processKillOptions).map {
                 arrayOf(it.first, it.second)
             }
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
similarity index 60%
copy from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
copy to benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
index 4ce15f7..33778b4 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -14,43 +14,37 @@
  * limitations under the License.
  */
 
-package androidx.compose.integration.macrobenchmark
+package androidx.benchmark.integration.macrobenchmark
 
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class) // Parameterized to work around timeouts (b/174175784)
-class StartupDemosMacrobenchmark(
-    private val ignored: Boolean
-) {
-
+@RunWith(Parameterized::class)
+class SmallListStartupBenchmark(private val startupMode: StartupMode) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
-    fun compiledColdStartup() = benchmarkRule.measureStartup(
+    fun startup() = benchmarkRule.measureStartup(
         profileCompiled = true,
-        coldLaunch = true
-    )
-
-    @Test
-    fun uncompiledColdStartup() = benchmarkRule.measureStartup(
-        profileCompiled = false,
-        coldLaunch = true
-    )
+        startupMode = startupMode
+    ) {
+        action = "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
+        putExtra("ITEM_COUNT", 5)
+    }
 
     companion object {
+        @Parameterized.Parameters(name = "mode={0}")
         @JvmStatic
-        @Parameterized.Parameters
-        fun startupDemosParameters(): List<Array<Any>> {
-            return listOf(arrayOf(false))
+        fun parameters(): List<Array<Any>> {
+            return listOf(StartupMode.COLD, StartupMode.WARM)
+                .map { arrayOf(it) }
         }
     }
 }
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
new file mode 100644
index 0000000..e8c152e
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+
+const val TARGET_PACKAGE = "androidx.benchmark.integration.macrobenchmark.target"
+
+fun MacrobenchmarkRule.measureStartup(
+    profileCompiled: Boolean,
+    startupMode: StartupMode,
+    iterations: Int = 5,
+    setupIntent: Intent.() -> Unit = {}
+) = measureStartupRepeated(
+    MacrobenchmarkConfig(
+        packageName = "androidx.benchmark.integration.macrobenchmark.target",
+        metrics = listOf(StartupTimingMetric()),
+        compilationMode = if (profileCompiled) {
+            CompilationMode.SpeedProfile(warmupIterations = 3)
+        } else {
+            CompilationMode.None
+        },
+        iterations = iterations
+    ),
+    startupMode = startupMode
+) {
+    pressHome()
+    val intent = Intent()
+    intent.setPackage(TARGET_PACKAGE)
+    setupIntent(intent)
+    launchIntentAndWait(intent)
+}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
similarity index 60%
copy from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
copy to benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
index 4ce15f7..af3e850 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -14,43 +14,36 @@
  * limitations under the License.
  */
 
-package androidx.compose.integration.macrobenchmark
+package androidx.benchmark.integration.macrobenchmark
 
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class) // Parameterized to work around timeouts (b/174175784)
-class StartupDemosMacrobenchmark(
-    private val ignored: Boolean
-) {
-
+@RunWith(Parameterized::class)
+class TrivialStartupBenchmark(private val startupMode: StartupMode) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
-    fun compiledColdStartup() = benchmarkRule.measureStartup(
+    fun startup() = benchmarkRule.measureStartup(
         profileCompiled = true,
-        coldLaunch = true
-    )
-
-    @Test
-    fun uncompiledColdStartup() = benchmarkRule.measureStartup(
-        profileCompiled = false,
-        coldLaunch = true
-    )
+        startupMode = startupMode
+    ) {
+        action = "androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
+    }
 
     companion object {
+        @Parameterized.Parameters(name = "mode={0}")
         @JvmStatic
-        @Parameterized.Parameters
-        fun startupDemosParameters(): List<Array<Any>> {
-            return listOf(arrayOf(false))
+        fun parameters(): List<Array<Any>> {
+            return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
+                .map { arrayOf(it) }
         }
     }
 }
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
index d614eb1..d5e016d 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
@@ -37,7 +37,7 @@
     @Test
     @Ignore("Figure out why we can't launch the default activity.")
     fun killTest() {
-        val scope = MacrobenchmarkScope(PACKAGE_NAME)
+        val scope = MacrobenchmarkScope(PACKAGE_NAME, launchWithClearTask = true)
         scope.pressHome()
         scope.launchPackageAndWait()
         assertTrue(isProcessAlive(PACKAGE_NAME))
@@ -48,7 +48,7 @@
     @Test
     @Ignore("Compilation modes are a bit flaky")
     fun compile_speedProfile() {
-        val scope = MacrobenchmarkScope(PACKAGE_NAME)
+        val scope = MacrobenchmarkScope(PACKAGE_NAME, launchWithClearTask = true)
         val iterations = 1
         var executions = 0
         val compilation = CompilationMode.SpeedProfile(warmupIterations = iterations)
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
index fa8afde..a2ece0e 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
@@ -56,7 +56,7 @@
         val packageName = "androidx.benchmark.macro.test"
         val helper = AppStartupHelper()
         helper.startCollecting()
-        val scope = MacrobenchmarkScope(packageName)
+        val scope = MacrobenchmarkScope(packageName, launchWithClearTask = false)
         // note: don't killProcess first, that's our process too!
         // additionally, skip pressHome, since it's not needed, and skipping saves significant time
         scope.launchPackageAndWait {
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
index 795f693..c0c35c3 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
@@ -89,7 +89,7 @@
         return List(expectedEventCount) { index ->
             val appStart = appStartOccurredList.getOrNull(index)
             AppStartupMetrics(
-                transitionType = appStart?.type.toString(),
+                transitionType = appStart?.type?.toStartupMode(),
                 windowDrawnDelayMs = appStart?.windowsDrawnDelayMillis?.toLong(),
                 transitionDelayMs = appStart?.transitionDelayMillis?.toLong(),
                 appStartupTimeMs = appStartFullyDrawnList.getOrNull(index)?.appStartupTimeMillis,
@@ -113,14 +113,20 @@
             )
         }
         val result = results.first()
-        // TODO: potentially filter these further for app vs platform usage
+
+        Log.d(
+            "AppStartupHelper",
+            "saw startup of package $packageName, type ${result.transitionType}"
+        )
         return mapOf(
             // AppStartupHelper originally reports this as simply startup time, so we do the same
             "startupMs" to result.windowDrawnDelayMs,
-            "transitionDelayMs" to result.transitionDelayMs,
             // Though the proto calls this appStartupTime, we clarify this is "fully drawn" startup
             "startupFullyDrawnMs" to result.appStartupTimeMs,
-            "processStartDelayMs" to result.processStartDelayMs,
+
+            // The following metrics are useful for platform devs, but disabled for now for brevity
+            // "transitionDelayMs" to result.transitionDelayMs,
+            // "processStartDelayMs" to result.processStartDelayMs,
         )
             .filterValues { it != null } // doesn't drop nullability for values...
             .mapValues { it.value!! } // ...so we do that explicitly here
@@ -140,8 +146,17 @@
         isProcStartDetailsDisabled = true
     }
 
+    private fun Int.toStartupMode(): StartupMode? {
+        return when (this) {
+            AtomsProto.AppStartOccurred.COLD -> StartupMode.COLD
+            AtomsProto.AppStartOccurred.WARM -> StartupMode.WARM
+            AtomsProto.AppStartOccurred.HOT -> StartupMode.HOT
+            else -> null
+        }
+    }
+
     data class AppStartupMetrics(
-        val transitionType: String?,
+        val transitionType: StartupMode?,
         val windowDrawnDelayMs: Long?,
         val transitionDelayMs: Long?,
         val appStartupTimeMs: Long?,
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 71ccadf..c386a1e 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -16,6 +16,8 @@
 
 package androidx.benchmark.macro
 
+import androidx.test.platform.app.InstrumentationRegistry
+
 sealed class CompilationMode(
     // for modes other than [None], is argument passed `cmd package compile`
     private val compileArgument: String?
@@ -39,3 +41,23 @@
         override fun toString() = "CompilationMode.Speed"
     }
 }
+
+internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
+    val instrumentation = InstrumentationRegistry.getInstrumentation()
+    // Clear profile between runs.
+    clearProfile(instrumentation, packageName)
+    if (this == CompilationMode.None) {
+        return // nothing to do
+    }
+    if (this is CompilationMode.SpeedProfile) {
+        repeat(this.warmupIterations) {
+            block()
+        }
+    }
+    // TODO: merge in below method
+    compilationFilter(
+        InstrumentationRegistry.getInstrumentation(),
+        packageName,
+        compileArgument()
+    )
+}
\ No newline at end of file
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
new file mode 100644
index 0000000..24e628d
--- /dev/null
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+import androidx.benchmark.Stats
+import java.util.Collections
+import kotlin.math.max
+
+internal fun ideSummaryString(benchmarkName: String, statsList: List<Stats>): String {
+    val maxLabelLength = Collections.max(statsList.map { it.name.length })
+
+    // max string length of any printed min/median/max is the largest max value seen. used to pad.
+    val maxValueLength = statsList
+        .map { it.max }
+        .reduce { acc, maxValue -> max(acc, maxValue) }
+        .toString().length
+
+    return "$benchmarkName\n" + statsList.joinToString("\n") {
+        val displayName = it.name.padStart(maxLabelLength)
+        val displayMin = it.min.toString().padStart(maxValueLength)
+        val displayMedian = it.median.toString().padStart(maxValueLength)
+        val displayMax = it.max.toString().padStart(maxValueLength)
+        "  $displayName   min $displayMin,   median $displayMedian,   max $displayMax"
+    } + "\n"
+}
\ No newline at end of file
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 66aac2d..0586e56 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -18,21 +18,28 @@
 
 import android.content.Intent
 import android.util.Log
+import androidx.benchmark.BenchmarkResult
 import androidx.benchmark.InstrumentationResults
-import androidx.benchmark.Stats
+import androidx.benchmark.MetricResult
+import androidx.benchmark.ResultWriter
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
-import java.util.Collections
-import kotlin.math.max
 
 /**
  * Provides access to common operations in app automation, such as killing the app,
  * or navigating home.
  */
 public class MacrobenchmarkScope(
-    private val packageName: String
+    private val packageName: String,
+    /**
+     * Controls whether launches will automatically set [Intent.FLAG_ACTIVITY_CLEAR_TASK].
+     *
+     * Default to true, so Activity launches go through full creation lifecycle stages, instead of
+     * just resume.
+     */
+    private val launchWithClearTask: Boolean
 ) {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context = instrumentation.context
@@ -40,14 +47,9 @@
 
     /**
      * Launch the package, with a customizable intent.
-     *
-     * If [block] is not specified, launches with [Intent.FLAG_ACTIVITY_NEW_TASK] as well as
-     * [Intent.FLAG_ACTIVITY_CLEAR_TASK]
      */
     fun launchPackageAndWait(
-        block: (Intent) -> Unit = {
-            it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-        }
+        block: (Intent) -> Unit = {}
     ) {
         val intent = context.packageManager.getLaunchIntentForPackage(packageName)
             ?: throw IllegalStateException("Unable to acquire intent for package $packageName")
@@ -57,6 +59,11 @@
     }
 
     fun launchIntentAndWait(intent: Intent) {
+        // Must launch with new task, as we're not launching from an existing task
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        if (launchWithClearTask) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        }
         context.startActivity(intent)
         device.wait(
             Until.hasObject(By.pkg(packageName).depth(0)),
@@ -79,7 +86,6 @@
     val packageName: String,
     val metrics: List<Metric>,
     val compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
-    val killProcessEachIteration: Boolean = false,
     val iterations: Int
 )
 
@@ -89,18 +95,22 @@
  * This function is a building block for public testing APIs
  */
 fun macrobenchmark(
-    benchmarkName: String,
+    uniqueName: String,
+    className: String,
+    testName: String,
     config: MacrobenchmarkConfig,
-    setupBlock: MacrobenchmarkScope.() -> Unit = {},
+    launchWithClearTask: Boolean,
+    setupBlock: MacrobenchmarkScope.(Boolean) -> Unit,
     measureBlock: MacrobenchmarkScope.() -> Unit
 ) = withPermissiveSeLinuxPolicy {
-    val scope = MacrobenchmarkScope(config.packageName)
+    val startTime = System.nanoTime()
+    val scope = MacrobenchmarkScope(config.packageName, launchWithClearTask)
 
     // always kill the process at beginning of test
     scope.killProcess()
 
     config.compilationMode.compile(config.packageName) {
-        setupBlock(scope)
+        setupBlock(scope, false)
         measureBlock(scope)
     }
 
@@ -111,12 +121,10 @@
         config.metrics.forEach {
             it.configure(config)
         }
-        val results = List(config.iterations) { iteration ->
-            if (config.killProcessEachIteration) {
-                // TODO: remove this flag, make this part of setupBlock
-                scope.killProcess()
-            }
-            setupBlock(scope)
+        var isFirstRun = true
+        val metricResults = List(config.iterations) { iteration ->
+            setupBlock(scope, isFirstRun)
+            isFirstRun = false
             try {
                 perfettoCollector.start()
                 config.metrics.forEach {
@@ -127,8 +135,7 @@
                 config.metrics.forEach {
                     it.stop()
                 }
-                val iterString = iteration.toString().padStart(3, '0')
-                perfettoCollector.stop("${benchmarkName}_iter$iterString.trace")
+                perfettoCollector.stop(uniqueName, iteration)
             }
 
             config.metrics
@@ -136,66 +143,106 @@
                 .map { it.getMetrics(config.packageName) }
                 // merge into one map
                 .reduce { sum, element -> sum + element }
-        }
-
-        // merge each independent Map<String,Long> to one Map<String,List<Long>>
-        val setOfAllKeys = results.flatMap { it.keys }.toSet()
-        val listResults = setOfAllKeys.map { key ->
-            // b/174175947
-            key to results.mapNotNull {
-                if (key !in it) {
-                    Log.w(TAG, "Value $key missing from one iteration {$it}")
-                }
-                it[key]
-            }
-        }.toMap()
-        val statsList = listResults.map { (metricName, values) ->
-            Stats(values.toLongArray(), metricName)
-        }.sortedBy { it.name }
+        }.mergeToMetricResults()
 
         InstrumentationResults.instrumentationReport {
-            ideSummaryRecord(ideSummaryString(benchmarkName, statsList))
+            val statsList = metricResults.map { it.stats }
+            ideSummaryRecord(ideSummaryString(uniqueName, statsList))
             statsList.forEach { it.putInBundle(bundle, "") }
         }
+
+        val warmupIterations = if (config.compilationMode is CompilationMode.SpeedProfile) {
+            config.compilationMode.warmupIterations
+        } else {
+            0
+        }
+
+        ResultWriter.appendReport(
+            BenchmarkResult(
+                className = className,
+                testName = testName,
+                totalRunTimeNs = System.nanoTime() - startTime,
+                metrics = metricResults,
+                repeatIterations = config.iterations,
+                thermalThrottleSleepSeconds = 0,
+                warmupIterations = warmupIterations
+            )
+        )
     } finally {
         scope.killProcess()
     }
 }
 
-fun ideSummaryString(benchmarkName: String, statsList: List<Stats>): String {
-    val maxLabelLength = Collections.max(statsList.map { it.name.length })
-
-    // max string length of any printed min/median/max is the largest max value seen. used to pad.
-    val maxValueLength = statsList
-        .map { it.max }
-        .reduce { acc, maxValue -> max(acc, maxValue) }
-        .toString().length
-
-    return "$benchmarkName\n" + statsList.joinToString("\n") {
-        val displayName = it.name.padStart(maxLabelLength)
-        val displayMin = it.min.toString().padStart(maxValueLength)
-        val displayMedian = it.median.toString().padStart(maxValueLength)
-        val displayMax = it.max.toString().padStart(maxValueLength)
-        "  $displayName   min $displayMin,   median $displayMedian,   max $displayMax"
-    } + "\n"
+/**
+ * Merge the Map<String, Long> results from each iteration into one Map<MetricResult>
+ */
+private fun List<Map<String, Long>>.mergeToMetricResults(): List<MetricResult> {
+    val setOfAllKeys = flatMap { it.keys }.toSet()
+    val listResults = setOfAllKeys.map { key ->
+        // b/174175947
+        key to mapNotNull {
+            if (key !in it) {
+                Log.w(TAG, "Value $key missing from one iteration {$it}")
+            }
+            it[key]
+        }
+    }.toMap()
+    return listResults.map { (metricName, values) ->
+        MetricResult(metricName, values.toLongArray())
+    }.sortedBy { it.stats.name }
 }
 
-internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
-    val instrumentation = InstrumentationRegistry.getInstrumentation()
-    // Clear profile between runs.
-    clearProfile(instrumentation, packageName)
-    if (this == CompilationMode.None) {
-        return // nothing to do
-    }
-    if (this is CompilationMode.SpeedProfile) {
-        repeat(this.warmupIterations) {
-            block()
-        }
-    }
-    // TODO: merge in below method
-    compilationFilter(
-        InstrumentationRegistry.getInstrumentation(),
-        packageName,
-        compileArgument()
+enum class StartupMode {
+    /**
+     * Startup from scratch - app's process is not alive, and must be started in addition to
+     * Activity creation.
+     *
+     * See
+     * [Cold startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#cold)
+     */
+    COLD,
+
+    /**
+     * Create and display a new Activity in a currently running app process.
+     *
+     * See
+     * [Warm startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#warm)
+     */
+    WARM,
+
+    /**
+     * Bring existing activity to the foreground, process and Activity still exist from previous
+     * launch.
+     *
+     * See
+     * [Hot startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#hot)
+     */
+    HOT
+}
+
+fun startupMacrobenchmark(
+    uniqueName: String,
+    className: String,
+    testName: String,
+    config: MacrobenchmarkConfig,
+    startupMode: StartupMode,
+    performStartup: MacrobenchmarkScope.() -> Unit
+) {
+    macrobenchmark(
+        uniqueName = uniqueName,
+        className = className,
+        testName = testName,
+        config = config,
+        setupBlock = { firstIterAfterCompile ->
+            if (startupMode == StartupMode.COLD) {
+                killProcess()
+            } else if (firstIterAfterCompile) {
+                // warmup process by launching the activity, unmeasured
+                performStartup()
+            }
+        },
+        // only reuse existing activity if StartupMode == HOT
+        launchWithClearTask = startupMode != StartupMode.HOT,
+        measureBlock = performStartup
     )
 }
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
index 86e31bc..0bc18a3 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
@@ -24,19 +24,42 @@
  * JUnit rule for benchmarking large app operations like startup.
  */
 class MacrobenchmarkRule : TestRule {
-    lateinit var benchmarkName: String
+    lateinit var currentDescription: Description
 
     fun measureRepeated(
         config: MacrobenchmarkConfig,
-        setupBlock: MacrobenchmarkScope.() -> Unit = {},
+        setupBlock: MacrobenchmarkScope.(Boolean) -> Unit = {},
         measureBlock: MacrobenchmarkScope.() -> Unit
     ) {
-        macrobenchmark(benchmarkName, config, setupBlock, measureBlock)
+        macrobenchmark(
+            uniqueName = currentDescription.toUniqueName(),
+            className = currentDescription.className,
+            testName = currentDescription.methodName,
+            config = config,
+            launchWithClearTask = true,
+            setupBlock = setupBlock,
+            measureBlock = measureBlock
+        )
+    }
+
+    fun measureStartupRepeated(
+        config: MacrobenchmarkConfig,
+        startupMode: StartupMode,
+        performStartup: MacrobenchmarkScope.() -> Unit
+    ) {
+        startupMacrobenchmark(
+            uniqueName = currentDescription.toUniqueName(),
+            className = currentDescription.className,
+            testName = currentDescription.methodName,
+            config = config,
+            startupMode = startupMode,
+            performStartup = performStartup
+        )
     }
 
     override fun apply(base: Statement, description: Description) = object : Statement() {
         override fun evaluate() {
-            benchmarkName = description.toUniqueName()
+            currentDescription = description
             base.evaluate()
         }
     }
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
index d94f6c0..aee2877 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -134,6 +134,10 @@
         "jank_percentile_90" to "frameTime90thPercentileMs",
         "jank_percentile_95" to "frameTime95thPercentileMs",
         "jank_percentile_99" to "frameTime99thPercentileMs",
+        "gpu_jank_percentile_50" to "gpuFrameTime50thPercentileMs",
+        "gpu_jank_percentile_90" to "gpuFrameTime90thPercentileMs",
+        "gpu_jank_percentile_95" to "gpuFrameTime95thPercentileMs",
+        "gpu_jank_percentile_99" to "gpuFrameTime99thPercentileMs",
         "missed_vsync" to "vsyncMissedFrameCount",
         "deadline_missed" to "deadlineMissedFrameCount",
         "janky_frames_count" to "jankyFrameCount",
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
index 88014df..840dee5 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
@@ -42,11 +42,14 @@
         return true
     }
 
-    fun stop(traceName: String): Boolean {
+    fun stop(benchmarkName: String, iteration: Int): Boolean {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            val iterString = iteration.toString().padStart(3, '0')
+            val traceName = "${benchmarkName}_iter$iterString.trace"
+
             val destination = destinationPath(traceName).absolutePath
             capture?.stop(destination)
-            reportAdditionalFileToCopy("perfetto_trace", destination)
+            reportAdditionalFileToCopy("perfetto_trace_$iterString", destination)
         }
         return true
     }
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/XmlTestConfigVerificationTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/XmlTestConfigVerificationTest.kt
deleted file mode 100644
index 43a6fe0..0000000
--- a/buildSrc-tests/src/test/kotlin/androidx/build/XmlTestConfigVerificationTest.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.xml.sax.InputSource
-import org.xml.sax.helpers.DefaultHandler
-import java.io.StringReader
-import javax.xml.parsers.SAXParserFactory
-
-/**
- * Simple check that the test config templates are able to be parsed as valid xml.
- */
-@RunWith(JUnit4::class)
-class XmlTestConfigVerificationTest {
-
-    @Test
-    fun testValidTestConfigXml_TEMPLATE() {
-        val parser = SAXParserFactory.newInstance().newSAXParser()
-        parser.parse(
-            InputSource(StringReader(TEMPLATE.replace("TEST_BLOCK", FULL_TEST))),
-            DefaultHandler()
-        )
-    }
-
-    @Test
-    fun testValidTestConfigXml_SELF_INSTRUMENTING_TEMPLATE() {
-        val parser = SAXParserFactory.newInstance().newSAXParser()
-        parser.parse(
-            InputSource(
-                StringReader(
-                    SELF_INSTRUMENTING_TEMPLATE.replace(
-                        "TEST_BLOCK",
-                        DEPENDENT_TESTS
-                    )
-                )
-            ),
-            DefaultHandler()
-        )
-    }
-
-    @Test
-    fun testValidTestConfigXml_MEDIA_TEMPLATE() {
-        val parser = SAXParserFactory.newInstance().newSAXParser()
-        parser.parse(
-            InputSource(
-                StringReader(
-                    MEDIA_TEMPLATE.replace(
-                        "INSTRUMENTATION_ARGS",
-                        CLIENT_PREVIOUS + SERVICE_PREVIOUS
-                    )
-                )
-            ),
-            DefaultHandler()
-        )
-    }
-}
\ No newline at end of file
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
index c187d48..cef9eac 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
@@ -48,7 +48,6 @@
     val tmpFolder2 = TemporaryFolder()
 
     private lateinit var root: Project
-    private lateinit var root2: Project
     private lateinit var p1: Project
     private lateinit var p2: Project
     private lateinit var p3: Project
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/XmlTestConfigVerificationTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/XmlTestConfigVerificationTest.kt
new file mode 100644
index 0000000..d35f5e8
--- /dev/null
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/XmlTestConfigVerificationTest.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.testConfiguration
+
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.xml.sax.InputSource
+import org.xml.sax.helpers.DefaultHandler
+import java.io.StringReader
+import javax.xml.parsers.SAXParserFactory
+
+/**
+ * Simple check that the test config templates are able to be parsed as valid xml.
+ */
+@RunWith(JUnit4::class)
+class XmlTestConfigVerificationTest {
+
+    private lateinit var builder: ConfigBuilder
+    private lateinit var mediaBuilder: MediaConfigBuilder
+
+    @Before
+    fun init() {
+        builder = ConfigBuilder()
+        builder.isBenchmark(false)
+            .applicationId("com.androidx.placeholder.Placeholder")
+            .isPostsubmit(true)
+            .minSdk("15")
+            .tag("placeholder_tag")
+            .testApkName("placeholder.apk")
+            .testRunner("com.example.Runner")
+        mediaBuilder = MediaConfigBuilder()
+        mediaBuilder.clientApplicationId("com.androidx.client.Placeholder")
+            .clientApkName("clientPlaceholder.apk")
+            .serviceApplicationId("com.androidx.service.Placeholder")
+            .serviceApkName("servicePlaceholder.apk")
+            .minSdk("15")
+            .tag("placeholder_tag")
+            .testRunner("com.example.Runner")
+            .isClientPrevious(true)
+            .isServicePrevious(false)
+    }
+
+    @Test
+    fun testAgainstGoldenDefault() {
+        MatcherAssert.assertThat(
+            builder.build(),
+            CoreMatchers.`is`(goldenDefaultConfig)
+        )
+    }
+
+    @Test
+    fun testAgainstMediaGoldenDefault() {
+        MatcherAssert.assertThat(
+            mediaBuilder.build(),
+            CoreMatchers.`is`(goldenMediaDefaultConfig)
+        )
+    }
+
+    @Test
+    fun testValidTestConfigXml_default() {
+        validate(builder.build())
+    }
+
+    @Test
+    fun testValidTestConfigXml_benchmarkTrue() {
+        builder.isBenchmark(true)
+        validate(builder.build())
+    }
+
+    @Test
+    fun testValidTestConfigXml_withAppApk() {
+        builder.appApkName("Placeholder.apk")
+        validate(builder.build())
+    }
+
+    @Test
+    fun testValidTestConfigXml_presubmitWithAppApk() {
+        builder.isPostsubmit(false)
+            .appApkName("Placeholder.apk")
+        validate(builder.build())
+    }
+
+    @Test
+    fun testValidTestConfigXml_presubmit() {
+        builder.isPostsubmit(false)
+        validate(builder.build())
+    }
+
+    @Test
+    fun testValidTestConfigXml_presubmitBenchmark() {
+        builder.isPostsubmit(false)
+            .isBenchmark(true)
+        validate(builder.build())
+    }
+
+    @Test
+    fun testValidMediaConfigXml_default() {
+        validate(mediaBuilder.build())
+    }
+
+    @Test
+    fun testValidMediaConfigXml_presubmit() {
+        mediaBuilder.isPostsubmit(false)
+        validate(mediaBuilder.build())
+    }
+
+    private fun validate(xml: String) {
+        val parser = SAXParserFactory.newInstance().newSAXParser()
+        return parser.parse(
+            InputSource(
+                StringReader(
+                    xml
+                )
+            ),
+            DefaultHandler()
+        )
+    }
+}
+
+private val goldenDefaultConfig = """
+    <?xml version="1.0" encoding="utf-8"?>
+    <!-- Copyright (C) 2020 The Android Open Source Project
+    Licensed under the Apache License, Version 2.0 (the "License")
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions
+    and limitations under the License.-->
+    <configuration description="Runs tests for the module">
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+    <option name="min-api-level" value="15" />
+    </object>
+    <option name="test-suite-tag" value="placeholder_tag" />
+    <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
+    <option name="wifi:disable" value="true" />
+    <include name="google/unbundled/common/setup" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+    <option name="cleanup-apks" value="true" />
+    <option name="test-file-name" value="placeholder.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+    <option name="runner" value="com.example.Runner"/>
+    <option name="package" value="com.androidx.placeholder.Placeholder" />
+    </test>
+    </configuration>
+""".trimIndent()
+
+private val goldenMediaDefaultConfig = """
+    <?xml version="1.0" encoding="utf-8"?>
+    <!-- Copyright (C) 2020 The Android Open Source Project
+    Licensed under the Apache License, Version 2.0 (the "License")
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions
+    and limitations under the License.-->
+    <configuration description="Runs tests for the module">
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+    <option name="min-api-level" value="15" />
+    </object>
+    <option name="test-suite-tag" value="placeholder_tag" />
+    <option name="test-suite-tag" value="media_compat" />
+    <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.client.Placeholder;com.androidx.service.Placeholder" />
+    <option name="wifi:disable" value="true" />
+    <include name="google/unbundled/common/setup" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+    <option name="cleanup-apks" value="true" />
+    <option name="test-file-name" value="clientPlaceholder.apk" />
+    <option name="test-file-name" value="servicePlaceholder.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+    <option name="runner" value="com.example.Runner"/>
+    <option name="package" value="com.androidx.client.Placeholder" />
+    <option name="instrumentation-arg" key="client_version" value="previous" />
+    <option name="instrumentation-arg" key="service_version" value="tot" />
+    </test>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+    <option name="runner" value="com.example.Runner"/>
+    <option name="package" value="com.androidx.service.Placeholder" />
+    <option name="instrumentation-arg" key="client_version" value="previous" />
+    <option name="instrumentation-arg" key="service_version" value="tot" />
+    </test>
+    </configuration>
+""".trimIndent()
\ No newline at end of file
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index bad8aa5..b2122db5 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -60,9 +60,7 @@
     cacheableImplementation {
         extendsFrom(project.configurations.cacheableApi)
     }
-    cacheableRuntime {
-        extendsFrom(project.configurations.cacheableImplementation)
-    }
+    cacheableRuntimeOnly
 }
 
 dependencies {
@@ -74,12 +72,12 @@
     cacheableApi build_libs.dokka_gradle
     // needed by inspection plugin
     cacheableImplementation "com.google.protobuf:protobuf-gradle-plugin:0.8.13"
-    // TODO(aurimas): remove when b/173417030 is fixed
+    // TODO(aurimas): remove when b/174658825 is fixed
     cacheableImplementation "org.anarres.jarjar:jarjar-gradle:1.0.1"
     cacheableImplementation "com.github.jengelman.gradle.plugins:shadow:5.2.0"
     // dependencies that aren't used by buildSrc directly but that we resolve here so that the
     // root project doesn't need to re-resolve them and their dependencies on every build
-    cacheableRuntime build_libs.hilt_plugin
+    cacheableRuntimeOnly build_libs.hilt_plugin
     // dependencies whose resolutions we don't need to cache
     compileOnly(findGradleKotlinDsl()) // Only one file in this configuration, no need to cache it
     implementation project("jetpad-integration") // Doesn't have a .pom, so not slow to load
@@ -168,7 +166,7 @@
 
 loadConfigurationQuicklyInto(configurations.cacheableApi, configurations.api)
 loadConfigurationQuicklyInto(configurations.cacheableImplementation, configurations.implementation)
-loadConfigurationQuicklyInto(configurations.cacheableRuntime, configurations.runtime)
+loadConfigurationQuicklyInto(configurations.cacheableRuntimeOnly, configurations.runtimeOnly)
 
 project.tasks.withType(Jar) { task ->
     task.reproducibleFileOrder = true
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
index 5fa294c..09e1a67 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
@@ -33,6 +33,8 @@
 import androidx.build.jacoco.Jacoco
 import androidx.build.license.configureExternalDependencyLicenseCheck
 import androidx.build.studio.StudioTask
+import androidx.build.testConfiguration.addAppApkToTestConfigGeneration
+import androidx.build.testConfiguration.configureTestConfigGeneration
 import com.android.build.api.extension.LibraryAndroidComponentsExtension
 import com.android.build.gradle.AppExtension
 import com.android.build.gradle.AppPlugin
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
index f27e132..600632c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
@@ -21,7 +21,6 @@
 import com.android.build.gradle.LibraryExtension
 import com.android.build.gradle.LibraryPlugin
 import com.android.build.gradle.TestedExtension
-import org.gradle.api.DomainObjectCollection
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.artifacts.type.ArtifactTypeDefinition
@@ -32,7 +31,6 @@
 import org.gradle.kotlin.dsl.invoke
 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
-import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 const val composeSourceOption =
@@ -194,7 +192,10 @@
                         "src/commonMain/kotlin", "src/jvmMain/kotlin",
                         "src/androidMain/kotlin"
                     )
-                    res.srcDirs("src/androidMain/res")
+                    res.srcDirs(
+                        "src/commonMain/resources",
+                        "src/androidMain/res"
+                    )
 
                     // Keep Kotlin files in java source sets so the source set is not empty when
                     // running unit tests which would prevent the tests from running in CI.
@@ -225,11 +226,9 @@
          * resolved.
          */
         private fun Project.configureForMultiplatform() {
-            if (multiplatformExtension == null) {
-                throw IllegalStateException(
-                    "Unable to configureForMultiplatform() when " +
-                        "multiplatformExtension is null (multiplatform plugin not enabled?)"
-                )
+            val multiplatformExtension = checkNotNull(multiplatformExtension) {
+                "Unable to configureForMultiplatform() when " +
+                    "multiplatformExtension is null (multiplatform plugin not enabled?)"
             }
 
             /*
@@ -247,13 +246,20 @@
             TODO: Consider changing unitTest to androidLocalTest and androidAndroidTest to
             androidDeviceTest when https://github.com/JetBrains/kotlin/pull/2829 rolls in.
             */
-            multiplatformExtension!!.sourceSets {
+            multiplatformExtension.sourceSets.all {
                 // Allow all experimental APIs, since MPP projects are themselves experimental
-                (this as DomainObjectCollection<KotlinSourceSet>).all {
-                    it.languageSettings.apply {
-                        useExperimentalAnnotation("kotlin.Experimental")
-                        useExperimentalAnnotation("kotlin.ExperimentalMultiplatform")
-                    }
+                it.languageSettings.apply {
+                    useExperimentalAnnotation("kotlin.Experimental")
+                    useExperimentalAnnotation("kotlin.ExperimentalMultiplatform")
+                }
+            }
+
+            afterEvaluate {
+                if (multiplatformExtension.targets.findByName("jvm") != null) {
+                    tasks.named("jvmTestClasses").also(::addToBuildOnServer)
+                }
+                if (multiplatformExtension.targets.findByName("desktop") != null) {
+                    tasks.named("desktopTestClasses").also(::addToBuildOnServer)
                 }
             }
         }
diff --git a/buildSrc/src/main/kotlin/androidx/build/GenerateMediaTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/GenerateMediaTestConfigurationTask.kt
deleted file mode 100644
index 3bfbd2f..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/GenerateMediaTestConfigurationTask.kt
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import com.android.build.api.variant.BuiltArtifacts
-import com.android.build.api.variant.BuiltArtifactsLoader
-import org.gradle.api.DefaultTask
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-
-const val MEDIA_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
-        <!-- Copyright (C) 2019 The Android Open Source Project
-        Licensed under the Apache License, Version 2.0 (the "License")
-        you may not use this file except in compliance with the License.
-        You may obtain a copy of the License at
-        http://www.apache.org/licenses/LICENSE-2.0
-
-        Unless required by applicable law or agreed to in writing, software
-        distributed under the License is distributed on an "AS IS" BASIS,
-        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-        See the License for the specific language governing permissions and
-        limitations under the License.
-        -->
-        <configuration description="Runs tests for the module">
-        <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
-            <option name="min-api-level" value="MIN_SDK" />
-        </object>
-        <option name="test-suite-tag" value="androidx_unit_tests_suite" />
-        <option name="config-descriptor:metadata" key="applicationId"
-            value="CLIENT_APPLICATION_ID;SERVICE_APPLICATION_ID" />
-        <option name="wifi:disable" value="true" />
-        <include name="google/unbundled/common/setup" />
-        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="CLIENT_FILE_NAME" />
-        <option name="test-file-name" value="SERVICE_FILE_NAME" />
-        </target_preparer>
-        <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="runner" value="TEST_RUNNER"/>
-        <option name="package" value="CLIENT_APPLICATION_ID" />
-        INSTRUMENTATION_ARGS
-        </test>
-        <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="runner" value="TEST_RUNNER"/>
-        <option name="package" value="SERVICE_APPLICATION_ID" />
-        INSTRUMENTATION_ARGS
-        </test>
-        </configuration>"""
-
-const val CLIENT_PREVIOUS = """
-    <option name="instrumentation-arg" key="client_version" value="previous" />
-"""
-const val CLIENT_TOT = """
-    <option name="instrumentation-arg" key="client_version" value="tot" />
-"""
-const val SERVICE_PREVIOUS = """
-    <option name="instrumentation-arg" key="service_version" value="previous" />
-"""
-const val SERVICE_TOT = """
-    <option name="instrumentation-arg" key="service_version" value="tot" />
-"""
-
-/**
- * Writes three configuration files to test combinations of media client & service in
- * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
- * format that gets zipped alongside the APKs to be tested. The combinations are of previous and
- * tip-of-tree versions client and service. We want to test every possible pairing that includes
- * tip-of-tree.
- *
- * This config gets ingested by Tradefed.
- */
-abstract class GenerateMediaTestConfigurationTask : DefaultTask() {
-
-    @get:InputFiles
-    abstract val clientToTFolder: DirectoryProperty
-
-    @get:Internal
-    abstract val clientToTLoader: Property<BuiltArtifactsLoader>
-
-    @get:InputFiles
-    abstract val clientPreviousFolder: DirectoryProperty
-
-    @get:Internal
-    abstract val clientPreviousLoader: Property<BuiltArtifactsLoader>
-
-    @get:InputFiles
-    abstract val serviceToTFolder: DirectoryProperty
-
-    @get:Internal
-    abstract val serviceToTLoader: Property<BuiltArtifactsLoader>
-
-    @get:InputFiles
-    abstract val servicePreviousFolder: DirectoryProperty
-
-    @get:Internal
-    abstract val servicePreviousLoader: Property<BuiltArtifactsLoader>
-
-    @get:Input
-    abstract val clientToTPath: Property<String>
-
-    @get:Input
-    abstract val clientPreviousPath: Property<String>
-
-    @get:Input
-    abstract val serviceToTPath: Property<String>
-
-    @get:Input
-    abstract val servicePreviousPath: Property<String>
-
-    @get:Input
-    abstract val minSdk: Property<Int>
-
-    @get:Input
-    abstract val testRunner: Property<String>
-
-    @get:OutputFile
-    abstract val clientPreviousServiceToT: RegularFileProperty
-
-    @get:OutputFile
-    abstract val clientToTServicePrevious: RegularFileProperty
-
-    @get:OutputFile
-    abstract val clientToTServiceToT: RegularFileProperty
-
-    @TaskAction
-    fun generateAndroidTestZip() {
-        val clientToTApk = resolveApk(clientToTFolder, clientToTLoader)
-        val clientPreviousApk = resolveApk(clientPreviousFolder, clientPreviousLoader)
-        val serviceToTApk = resolveApk(serviceToTFolder, serviceToTLoader)
-        val servicePreviousApk = resolveApk(
-            servicePreviousFolder, servicePreviousLoader
-        )
-        writeConfigFileContent(
-            clientToTApk, serviceToTApk, clientToTPath.get(),
-            serviceToTPath.get(), clientToTServiceToT
-        )
-        writeConfigFileContent(
-            clientToTApk, servicePreviousApk, clientToTPath.get(),
-            servicePreviousPath.get(), clientToTServicePrevious
-        )
-        writeConfigFileContent(
-            clientPreviousApk, serviceToTApk, clientPreviousPath.get(),
-            serviceToTPath.get(), clientPreviousServiceToT
-        )
-    }
-
-    private fun resolveApk(
-        apkFolder: DirectoryProperty,
-        apkLoader: Property<BuiltArtifactsLoader>
-    ): BuiltArtifacts {
-        return apkLoader.get().load(apkFolder.get())
-            ?: throw RuntimeException("Cannot load APK for $name")
-    }
-
-    private fun resolveName(apk: BuiltArtifacts, path: String): String {
-        return apk.elements.single().outputFile.substringAfterLast("/")
-            .renameApkForTesting(path, false)
-    }
-
-    private fun writeConfigFileContent(
-        clientApk: BuiltArtifacts,
-        serviceApk: BuiltArtifacts,
-        clientPath: String,
-        servicePath: String,
-        outputFile: RegularFileProperty
-    ) {
-        val instrumentationArgs =
-            if (clientPath.contains("previous")) {
-                if (servicePath.contains("previous")) {
-                    CLIENT_PREVIOUS + SERVICE_PREVIOUS
-                } else {
-                    CLIENT_PREVIOUS + SERVICE_TOT
-                }
-            } else if (servicePath.contains("previous")) {
-                CLIENT_TOT + SERVICE_PREVIOUS
-            } else {
-                CLIENT_TOT + SERVICE_TOT
-            }
-        var configContent: String = MEDIA_TEMPLATE
-        configContent = configContent
-            .replace("CLIENT_FILE_NAME", resolveName(clientApk, clientPath))
-            .replace("SERVICE_FILE_NAME", resolveName(serviceApk, servicePath))
-            .replace("CLIENT_APPLICATION_ID", clientApk.applicationId)
-            .replace("SERVICE_APPLICATION_ID", serviceApk.applicationId)
-            .replace("MIN_SDK", minSdk.get().toString())
-            .replace("TEST_RUNNER", testRunner.get())
-            .replace("INSTRUMENTATION_ARGS", instrumentationArgs)
-        val resolvedOutputFile: File = outputFile.asFile.get()
-        if (!resolvedOutputFile.exists()) {
-            if (!resolvedOutputFile.createNewFile()) {
-                throw RuntimeException(
-                    "Failed to create test configuration file: $outputFile"
-                )
-            }
-        }
-        resolvedOutputFile.writeText(configContent)
-    }
-}
diff --git a/buildSrc/src/main/kotlin/androidx/build/GenerateTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/GenerateTestConfigurationTask.kt
deleted file mode 100644
index e958cd3..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/GenerateTestConfigurationTask.kt
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import androidx.build.dependencyTracker.ProjectSubset
-import com.android.build.api.variant.BuiltArtifactsLoader
-import org.gradle.api.DefaultTask
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-
-const val TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
-        <!-- Copyright (C) 2019 The Android Open Source Project
-        Licensed under the Apache License, Version 2.0 (the "License")
-        you may not use this file except in compliance with the License.
-        You may obtain a copy of the License at
-        http://www.apache.org/licenses/LICENSE-2.0
-
-        Unless required by applicable law or agreed to in writing, software
-        distributed under the License is distributed on an "AS IS" BASIS,
-        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-        See the License for the specific language governing permissions and
-        limitations under the License.
-        -->
-        <configuration description="Runs tests for the module">
-        <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
-            <option name="min-api-level" value="MIN_SDK" />
-        </object>
-        <option name="test-suite-tag" value="TEST_SUITE_TAG" />
-        <option name="config-descriptor:metadata" key="applicationId" value="APPLICATION_ID" />
-        <option name="wifi:disable" value="true" />
-        <include name="google/unbundled/common/setup" />
-        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="TEST_FILE_NAME" />
-        <option name="test-file-name" value="APP_FILE_NAME" />
-        </target_preparer>
-        <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="runner" value="TEST_RUNNER"/>
-        <option name="package" value="APPLICATION_ID" />
-        </test>
-        </configuration>"""
-
-const val SELF_INSTRUMENTING_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
-        <!-- Copyright (C) 2019 The Android Open Source Project
-        Licensed under the Apache License, Version 2.0 (the "License")
-        you may not use this file except in compliance with the License.
-        You may obtain a copy of the License at
-        http://www.apache.org/licenses/LICENSE-2.0
-
-        Unless required by applicable law or agreed to in writing, software
-        distributed under the License is distributed on an "AS IS" BASIS,
-        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-        See the License for the specific language governing permissions and
-        limitations under the License.
-        -->
-        <configuration description="Runs tests for the module">
-        <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
-            <option name="min-api-level" value="MIN_SDK" />
-        </object>
-        <option name="test-suite-tag" value="TEST_SUITE_TAG" />
-        <option name="config-descriptor:metadata" key="applicationId" value="APPLICATION_ID" />
-        <option name="wifi:disable" value="true" />
-        <include name="google/unbundled/common/setup" />
-        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="TEST_FILE_NAME" />
-        </target_preparer>
-        TEST_BLOCK
-        </configuration>"""
-
-const val FULL_TEST = """
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="runner" value="TEST_RUNNER"/>
-        <option name="package" value="APPLICATION_ID" />
-    </test>
-"""
-
-const val DEPENDENT_TESTS = """
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="runner" value="TEST_RUNNER"/>
-        <option name="package" value="APPLICATION_ID" />
-        <option name="size" value="small" />
-        <option name="test-timeout" value="300" />
-    </test>
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="runner" value="TEST_RUNNER"/>
-        <option name="package" value="APPLICATION_ID" />
-        <option name="size" value="medium" />
-        <option name="test-timeout" value="1500" />
-    </test>
-"""
-
-/**
- * Writes a configuration file in
- * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
- * format that gets zipped alongside the APKs to be tested.
- * This config gets ingested by Tradefed.
- */
-abstract class GenerateTestConfigurationTask : DefaultTask() {
-
-    @get:InputFiles
-    @get:Optional
-    abstract val appFolder: DirectoryProperty
-
-    @get:Internal
-    abstract val appLoader: Property<BuiltArtifactsLoader>
-
-    @get:InputFiles
-    abstract val testFolder: DirectoryProperty
-
-    @get:Internal
-    abstract val testLoader: Property<BuiltArtifactsLoader>
-
-    @get:Input
-    abstract val minSdk: Property<Int>
-
-    @get:Input
-    abstract val hasBenchmarkPlugin: Property<Boolean>
-
-    @get:Input
-    abstract val testRunner: Property<String>
-
-    @get:Input
-    abstract val projectPath: Property<String>
-
-    @get:Input
-    abstract val affectedModuleDetectorSubset: Property<ProjectSubset>
-
-    @get:OutputFile
-    abstract val outputXml: RegularFileProperty
-
-    @TaskAction
-    fun generateAndroidTestZip() {
-        writeConfigFileContent()
-    }
-
-    private fun writeConfigFileContent() {
-        /*
-        Testing an Android Application project involves 2 APKS: an application to be instrumented,
-        and a test APK. Testing an Android Library project involves only 1 APK, since the library
-        is bundled inside the test APK, meaning it is self instrumenting. We add extra data to
-        configurations testing Android Application projects, so that both APKs get installed.
-         */
-        var configContent: String = if (appLoader.isPresent) {
-            val appApk = appLoader.get().load(appFolder.get())
-                ?: throw RuntimeException("Cannot load application APK for $name")
-            val appName = appApk.elements.single().outputFile.substringAfterLast("/")
-                .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
-            TEMPLATE.replace("APP_FILE_NAME", appName)
-        } else {
-            SELF_INSTRUMENTING_TEMPLATE
-        }
-        configContent = when (affectedModuleDetectorSubset.get()) {
-            ProjectSubset.CHANGED_PROJECTS, ProjectSubset.ALL_AFFECTED_PROJECTS -> {
-                configContent.replace("TEST_BLOCK", FULL_TEST)
-            }
-            ProjectSubset.DEPENDENT_PROJECTS -> {
-                configContent.replace("TEST_BLOCK", DEPENDENT_TESTS)
-            }
-            else -> {
-                throw IllegalStateException(
-                    "$name should not be running if the AffectedModuleDetector is returning " +
-                        "${affectedModuleDetectorSubset.get()} for this project."
-                )
-            }
-        }
-        val tag = if (hasBenchmarkPlugin.get()) "MetricTests" else "androidx_unit_tests"
-        val testApk = testLoader.get().load(testFolder.get())
-            ?: throw RuntimeException("Cannot load test APK for $name")
-        val testName = testApk.elements.single().outputFile
-            .substringAfterLast("/")
-            .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
-        configContent = configContent.replace("TEST_FILE_NAME", testName)
-            .replace("APPLICATION_ID", testApk.applicationId)
-            .replace("MIN_SDK", minSdk.get().toString())
-            .replace("TEST_SUITE_TAG", tag)
-            .replace("TEST_RUNNER", testRunner.get())
-        val resolvedOutputFile: File = outputXml.asFile.get()
-        if (!resolvedOutputFile.exists()) {
-            if (!resolvedOutputFile.createNewFile()) {
-                throw RuntimeException(
-                    "Failed to create test configuration file: $outputXml"
-                )
-            }
-        }
-        resolvedOutputFile.writeText(configContent)
-    }
-}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 0719b92..d9f7cdc 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -20,7 +20,7 @@
  * The list of versions codes of all the libraries in this project.
  */
 object LibraryVersions {
-    val ACTIVITY = Version("1.2.0-beta02")
+    val ACTIVITY = Version("1.2.0-rc01")
     val ADS_IDENTIFIER = Version("1.0.0-alpha04")
     val ANNOTATION = Version("1.2.0-alpha02")
     val ANNOTATION_EXPERIMENTAL = Version("1.1.0-alpha02")
@@ -32,7 +32,7 @@
     val ASYNCLAYOUTINFLATER = Version("1.1.0-alpha01")
     val AUTOFILL = Version("1.1.0-rc01")
     val BENCHMARK = Version("1.1.0-alpha02")
-    val BIOMETRIC = Version("1.2.0-alpha01")
+    val BIOMETRIC = Version("1.2.0-alpha02")
     val BROWSER = Version("1.3.0-rc01")
     val BUILDSRC_TESTS = Version("1.0.0-alpha01")
     val CAMERA = Version("1.0.0-rc01")
@@ -44,7 +44,7 @@
     val CAR_APP = Version("1.0.0-alpha01")
     val COLLECTION = Version("1.2.0-alpha01")
     val CONTENTPAGER = Version("1.1.0-alpha01")
-    val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha08")
+    val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha09")
     val COORDINATORLAYOUT = Version("1.2.0-alpha01")
     val CORE = Version("1.5.0-alpha06")
     val CORE_ANIMATION = Version("1.0.0-alpha03")
@@ -61,7 +61,7 @@
     val EMOJI = Version("1.2.0-alpha03")
     val ENTERPRISE = Version("1.1.0-rc01")
     val EXIFINTERFACE = Version("1.4.0-alpha01")
-    val FRAGMENT = Version("1.3.0-beta02")
+    val FRAGMENT = Version("1.3.0-rc01")
     val FUTURES = Version("1.2.0-alpha01")
     val GRIDLAYOUT = Version("1.1.0-alpha01")
     val HEIFWRITER = Version("1.1.0-alpha02")
@@ -71,20 +71,21 @@
     val IPC = Version("1.0.0-alpha01")
     val JETIFIER = Version("1.0.0-beta10")
     val LEANBACK = Version("1.1.0-beta01")
-    val LEANBACK_PAGING = Version("1.1.0-alpha06")
+    val LEANBACK_PAGING = Version("1.1.0-alpha07")
     val LEANBACK_PREFERENCE = Version("1.1.0-beta01")
+    val LEANBACK_TAB = Version("1.1.0-beta01")
     val LEGACY = Version("1.1.0-alpha01")
     val LOCALBROADCASTMANAGER = Version("1.1.0-alpha02")
-    val LIFECYCLE = Version("2.3.0-beta01")
+    val LIFECYCLE = Version("2.3.0-rc01")
     val LIFECYCLE_EXTENSIONS = Version("2.2.0")
     val LOADER = Version("1.2.0-alpha01")
     val MEDIA = Version("1.3.0-alpha02")
     val MEDIA2 = Version("1.2.0-alpha01")
     val MEDIAROUTER = Version("1.3.0-alpha01")
     val NAVIGATION = Version("2.4.0-alpha01")
-    val NAVIGATION_COMPOSE = Version("1.0.0-alpha03")
-    val PAGING = Version("3.0.0-alpha10")
-    val PAGING_COMPOSE = Version("1.0.0-alpha03")
+    val NAVIGATION_COMPOSE = Version("1.0.0-alpha04")
+    val PAGING = Version("3.0.0-alpha11")
+    val PAGING_COMPOSE = Version("1.0.0-alpha04")
     val PALETTE = Version("1.1.0-alpha01")
     val PRINT = Version("1.1.0-beta01")
     val PERCENTLAYOUT = Version("1.1.0-alpha01")
@@ -94,12 +95,13 @@
     val RECYCLERVIEW_SELECTION = Version("2.0.0-alpha01")
     val REMOTECALLBACK = Version("1.0.0-alpha02")
     val ROOM = Version("2.3.0-alpha04")
-    val SAVEDSTATE = Version("1.1.0-beta01")
+    val SAVEDSTATE = Version("1.1.0-rc01")
     val SECURITY = Version("1.1.0-alpha03")
+    val SECURITY_APP_AUTHENTICATOR = Version("1.0.0-alpha01")
     val SECURITY_BIOMETRIC = Version("1.0.0-alpha01")
     val SECURITY_IDENTITY_CREDENTIAL = Version("1.0.0-alpha01")
     val SERIALIZATION = Version("1.0.0-alpha01")
-    val SHARETARGET = Version("1.1.0-rc01")
+    val SHARETARGET = Version("1.2.0-alpha01")
     val SLICE = Version("1.1.0-alpha02")
     val SLICE_BENCHMARK = Version("1.1.0-alpha02")
     val SLICE_BUILDERS_KTX = Version("1.0.0-alpha08")
@@ -112,7 +114,7 @@
     val TESTSCREENSHOT = Version("1.0.0-alpha01")
     val TEXT = Version("1.0.0-alpha01")
     val TEXTCLASSIFIER = Version("1.0.0-alpha03")
-    val TRACING = Version("1.0.0")
+    val TRACING = Version("1.1.0-alpha01")
     val TRANSITION = Version("1.4.0-rc01")
     val TVPROVIDER = Version("1.1.0-alpha02")
     val VECTORDRAWABLE = Version("1.2.0-alpha03")
@@ -121,15 +123,15 @@
     val VERSIONED_PARCELABLE = Version("1.2.0-alpha01")
     val VIEWPAGER = Version("1.1.0-alpha01")
     val VIEWPAGER2 = Version("1.1.0-alpha02")
-    val WEAR = Version("1.2.0-alpha03")
-    val WEAR_COMPLICATIONS = Version("1.0.0-alpha03")
-    val WEAR_INPUT = Version("1.0.0-rc01")
+    val WEAR = Version("1.2.0-alpha04")
+    val WEAR_COMPLICATIONS = Version("1.0.0-alpha04")
+    val WEAR_INPUT = Version("1.1.0-alpha01")
     val WEAR_TILES = Version("1.0.0-alpha01")
     val WEAR_TILES_DATA = WEAR_TILES
-    val WEAR_WATCHFACE = Version("1.0.0-alpha03")
-    val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha03")
-    val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha03")
-    val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha03")
+    val WEAR_WATCHFACE = Version("1.0.0-alpha04")
+    val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha04")
+    val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha04")
+    val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha04")
     val WEBKIT = Version("1.4.0-beta01")
     val WINDOW = Version("1.0.0-alpha02")
     val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
index a29179c..9c6b2c0 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -91,7 +91,6 @@
             isCheckReleaseBuilds = false
 
             // Write output directly to the console (and nowhere else).
-            textOutput("stderr")
             textReport = true
             htmlReport = false
 
@@ -111,10 +110,15 @@
             // concerned with drawables potentially being a little bit blurry
             disable("IconMissingDensityFolder")
 
+            // Disable a check that's only triggered by translation updates which are
+            // outside of library owners' control, b/174655193
+            disable("UnusedQuantity")
+
             // Disable until it works for our projects, b/171986505
             disable("JavaPluginLanguageLevel")
 
-            if (extension.type.compilationTarget != CompilationTarget.HOST) {
+            // Provide stricter enforcement for project types intended to run on a device.
+            if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
                 fatal("Assert")
                 fatal("NewApi")
                 fatal("ObsoleteSdkInt")
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 02db235..eba1f5c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -77,7 +77,7 @@
     "com.squareup:kotlinpoet-classinspector-elements:1.4.0"
 const val KOTLIN_COMPILE_TESTING = "com.github.tschuchortdev:kotlin-compile-testing:1.3.1"
 const val KOTLIN_COMPILE_TESTING_KSP = "com.github.tschuchortdev:kotlin-compile-testing-ksp:1.3.1"
-const val KSP_VERSION = "1.4.10-dev-experimental-20201120"
+const val KSP_VERSION = "1.4.20-dev-experimental-20201204"
 const val KOTLIN_KSP_API = "com.google.devtools.ksp:symbol-processing-api:$KSP_VERSION"
 const val KOTLIN_KSP = "com.google.devtools.ksp:symbol-processing:$KSP_VERSION"
 const val KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20"
@@ -98,7 +98,7 @@
 const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
 const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
 const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.16"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.18"
 val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
 val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
 val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
@@ -154,6 +154,12 @@
 val KOTLIN_TEST_JUNIT get() = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
 val KOTLIN_TEST_JS get() = "org.jetbrains.kotlin:kotlin-test-js:$kotlinVersion"
 val KOTLIN_REFLECT get() = "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
+val KOTLIN_COMPILER_EMBEDDABLE
+    get() = "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion"
+val KOTLIN_COMPILER_DAEMON_EMBEDDABLE
+    get() = "org.jetbrains.kotlin:kotlin-daemon-embeddable:$kotlinVersion"
+val KOTLIN_ANNOTATION_PROCESSING_EMBEDDABLE
+    get() = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable:$kotlinVersion"
 
 internal lateinit var kotlinCoroutinesVersion: String
 
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index 4d7315f5..199694d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -470,12 +470,19 @@
         private val COBUILT_TEST_PATHS = setOf(
             // Install media tests together per b/128577735
             setOf(
+                // Making a change in :media:version-compat-tests makes
+                // mediaGenerateTestConfiguration run (an unfortunate but low priority bug). To
+                // prevent failures from missing apks, we make sure to build the
+                // version-compat-tests projects in that case. Same with media2-session below.
+                ":media:version-compat-tests",
                 ":media:version-compat-tests:client",
                 ":media:version-compat-tests:service",
                 ":media:version-compat-tests:client-previous",
                 ":media:version-compat-tests:service-previous"
             ),
             setOf(
+                ":media2:media2-session",
+                ":media2:media2-session:version-compat-tests",
                 ":media2:media2-session:version-compat-tests:client",
                 ":media2:media2-session:version-compat-tests:service",
                 ":media2:media2-session:version-compat-tests:client-previous",
@@ -486,17 +493,17 @@
                 ":compose:material:material"
             ),
             setOf(
-                ":benchmark:integration-tests:macrobenchmark",
-                ":benchmark:integration-tests:macrobenchmark-target"
-            ),
-            setOf(
                 ":benchmark:benchmark-macro",
                 ":benchmark:integration-tests:macrobenchmark-target"
-            ),
+            ), // link benchmark-macro's correctness test and its target
+            setOf(
+                ":benchmark:integration-tests:macrobenchmark",
+                ":benchmark:integration-tests:macrobenchmark-target"
+            ), // link benchmark's macrobenchmark and its target
             setOf(
                 ":compose:integration-tests:macrobenchmark",
-                ":compose:integration-tests:demos"
-            ),
+                ":compose:integration-tests:macrobenchmark-target"
+            ), // link compose's macrobenchmark and its target
         )
     }
 }
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt
index 359edbb..eb0d0e0 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt
@@ -39,7 +39,7 @@
     ).also {
         it.level = LogLevel.DEBUG
         it.setOutputEventListener {
-            stringBuilder.appendln(it.toString())
+            stringBuilder.append(it.toString() + "\n")
         }
     },
     Clock {
@@ -65,4 +65,4 @@
             return logger
         }
     }
-}
\ No newline at end of file
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt b/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt
index 47148e3..2da678f 100644
--- a/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt
@@ -67,7 +67,8 @@
                     it.from(v.testedVariant.javaCompileProvider.get().destinationDir)
                     it.exclude("**/R.class", "**/R\$*.class", "**/BuildConfig.class")
                     it.destinationDirectory.set(project.buildDir)
-                    it.archiveFileName.set("${project.name}-${v.baseName}-allclasses.jar")
+                    val sanitizedPath = project.path.removePrefix(":").replace(':', '_')
+                    it.archiveFileName.set("$sanitizedPath-${v.baseName}-allclasses.jar")
                 }
                 project.rootProject.tasks.named(
                     "packageAllClassFilesForCoverageReport",
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
new file mode 100644
index 0000000..a55e365
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.testConfiguration
+
+class ConfigBuilder {
+    var appApkName: String? = null
+    lateinit var applicationId: String
+    var isBenchmark: Boolean = false
+    var isPostsubmit: Boolean = true
+    lateinit var minSdk: String
+    var tag: String = "androidx_unit_tests"
+    lateinit var testApkName: String
+    lateinit var testRunner: String
+
+    fun appApkName(appApkName: String) = apply { this.appApkName = appApkName }
+    fun applicationId(applicationId: String) = apply { this.applicationId = applicationId }
+    fun isBenchmark(isBenchmark: Boolean) = apply { this.isBenchmark = isBenchmark }
+    fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
+    fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
+    fun tag(tag: String) = apply { this.tag = tag }
+    fun testApkName(testApkName: String) = apply { this.testApkName = testApkName }
+    fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
+
+    fun build(): String {
+        val sb = StringBuilder()
+        sb.append(XML_HEADER_AND_LICENSE)
+            .append(CONFIGURATION_OPEN)
+            .append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk))
+            .append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag))
+            .append(MODULE_METADATA_TAG_OPTION.replace("APPLICATION_ID", applicationId))
+            .append(WIFI_DISABLE_OPTION)
+        if (isBenchmark) {
+            if (isPostsubmit) {
+                sb.append(BENCHMARK_POSTSUBMIT_OPTIONS)
+            } else {
+                sb.append(BENCHMARK_PRESUBMIT_OPTION)
+            }
+        }
+        sb.append(SETUP_INCLUDE)
+            .append(TARGET_PREPARER_OPEN)
+            .append(APK_INSTALL_OPTION.replace("APK_NAME", testApkName))
+        if (!appApkName.isNullOrEmpty())
+            sb.append(APK_INSTALL_OPTION.replace("APK_NAME", appApkName!!))
+        sb.append(TARGET_PREPARER_CLOSE)
+            .append(TEST_BLOCK_OPEN)
+            .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+            .append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
+        if (isPostsubmit)
+            sb.append(TEST_BLOCK_CLOSE)
+        else {
+            sb.append(SMALL_TEST_OPTIONS)
+                .append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_OPEN)
+                .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+                .append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
+                .append(MEDIUM_TEST_OPTIONS)
+                .append(TEST_BLOCK_CLOSE)
+        }
+        sb.append(CONFIGURATION_CLOSE)
+        return sb.toString()
+    }
+}
+
+class MediaConfigBuilder {
+    lateinit var clientApkName: String
+    lateinit var clientApplicationId: String
+    var isClientPrevious: Boolean = true
+    var isPostsubmit: Boolean = true
+    var isServicePrevious: Boolean = true
+    lateinit var minSdk: String
+    lateinit var serviceApkName: String
+    lateinit var serviceApplicationId: String
+    var tag: String = "androidx_unit_tests"
+    lateinit var testRunner: String
+
+    fun clientApkName(clientApkName: String) = apply { this.clientApkName = clientApkName }
+    fun clientApplicationId(clientApplicationId: String) =
+        apply { this.clientApplicationId = clientApplicationId }
+    fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
+    fun isClientPrevious(isClientPrevious: Boolean) = apply {
+        this.isClientPrevious = isClientPrevious
+    }
+    fun isServicePrevious(isServicePrevious: Boolean) = apply {
+        this.isServicePrevious = isServicePrevious
+    }
+    fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
+    fun serviceApkName(serviceApkName: String) = apply { this.serviceApkName = serviceApkName }
+    fun serviceApplicationId(serviceApplicationId: String) =
+        apply { this.serviceApplicationId = serviceApplicationId }
+    fun tag(tag: String) = apply { this.tag = tag }
+    fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
+
+    private fun mediaInstrumentationArgs(): String {
+        return if (isClientPrevious) {
+            if (isServicePrevious) {
+                CLIENT_PREVIOUS + SERVICE_PREVIOUS
+            } else {
+                CLIENT_PREVIOUS + SERVICE_TOT
+            }
+        } else {
+            if (isServicePrevious) {
+                CLIENT_TOT + SERVICE_PREVIOUS
+            } else {
+                CLIENT_TOT + SERVICE_TOT
+            }
+        }
+    }
+
+    fun build(): String {
+        val sb = StringBuilder()
+        sb.append(XML_HEADER_AND_LICENSE)
+            .append(CONFIGURATION_OPEN)
+            .append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk))
+            .append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag))
+            .append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", "media_compat"))
+            .append(
+                MODULE_METADATA_TAG_OPTION.replace(
+                    "APPLICATION_ID", "$clientApplicationId;$serviceApplicationId"
+                )
+            )
+            .append(WIFI_DISABLE_OPTION)
+            .append(SETUP_INCLUDE)
+            .append(TARGET_PREPARER_OPEN)
+            .append(APK_INSTALL_OPTION.replace("APK_NAME", clientApkName))
+            .append(APK_INSTALL_OPTION.replace("APK_NAME", serviceApkName))
+        sb.append(TARGET_PREPARER_CLOSE)
+            .append(TEST_BLOCK_OPEN)
+            .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+            .append(PACKAGE_OPTION.replace("APPLICATION_ID", clientApplicationId))
+            .append(mediaInstrumentationArgs())
+        if (isPostsubmit)
+            sb.append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_OPEN)
+                .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+                .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
+                .append(mediaInstrumentationArgs())
+                .append(TEST_BLOCK_CLOSE)
+        else {
+            // add the small and medium test runners for both client and service apps
+            sb.append(SMALL_TEST_OPTIONS)
+                .append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_OPEN)
+                .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+                .append(PACKAGE_OPTION.replace("APPLICATION_ID", clientApplicationId))
+                .append(mediaInstrumentationArgs())
+                .append(MEDIUM_TEST_OPTIONS)
+                .append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_OPEN)
+                .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+                .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
+                .append(mediaInstrumentationArgs())
+                .append(SMALL_TEST_OPTIONS)
+                .append(TEST_BLOCK_CLOSE)
+                .append(TEST_BLOCK_OPEN)
+                .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+                .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
+                .append(mediaInstrumentationArgs())
+                .append(MEDIUM_TEST_OPTIONS)
+                .append(TEST_BLOCK_CLOSE)
+        }
+        sb.append(CONFIGURATION_CLOSE)
+        return sb.toString()
+    }
+}
+
+/**
+ * These constants are the building blocks of the xml configs, but
+ * they aren't very readable as separate chunks. Look to
+ * the golden examples at the bottom of
+ * {@link androidx.build.testConfiguration.XmlTestConfigVerificationTest}
+ * for examples of what the full xml will look like.
+ */
+
+private val XML_HEADER_AND_LICENSE = """
+    <?xml version="1.0" encoding="utf-8"?>
+    <!-- Copyright (C) 2020 The Android Open Source Project
+    Licensed under the Apache License, Version 2.0 (the "License")
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions
+    and limitations under the License.-->
+
+""".trimIndent()
+
+private val CONFIGURATION_OPEN = """
+    <configuration description="Runs tests for the module">
+
+""".trimIndent()
+
+private val CONFIGURATION_CLOSE = """
+    </configuration>
+""".trimIndent()
+
+private val MIN_API_LEVEL_CONTROLLER_OBJECT = """
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+    <option name="min-api-level" value="MIN_SDK" />
+    </object>
+
+""".trimIndent()
+
+private val TEST_SUITE_TAG_OPTION = """
+    <option name="test-suite-tag" value="TEST_SUITE_TAG" />
+
+""".trimIndent()
+
+private val MODULE_METADATA_TAG_OPTION = """
+    <option name="config-descriptor:metadata" key="applicationId" value="APPLICATION_ID" />
+
+""".trimIndent()
+
+private val WIFI_DISABLE_OPTION = """
+    <option name="wifi:disable" value="true" />
+
+""".trimIndent()
+
+private val SETUP_INCLUDE = """
+    <include name="google/unbundled/common/setup" />
+
+""".trimIndent()
+
+private val TARGET_PREPARER_OPEN = """
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+    <option name="cleanup-apks" value="true" />
+
+""".trimIndent()
+
+private val TARGET_PREPARER_CLOSE = """
+    </target_preparer>
+
+""".trimIndent()
+
+private val APK_INSTALL_OPTION = """
+    <option name="test-file-name" value="APK_NAME" />
+
+""".trimIndent()
+
+private val TEST_BLOCK_OPEN = """
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+
+""".trimIndent()
+
+private val TEST_BLOCK_CLOSE = """
+    </test>
+
+""".trimIndent()
+
+private val RUNNER_OPTION = """
+    <option name="runner" value="TEST_RUNNER"/>
+
+""".trimIndent()
+
+private val PACKAGE_OPTION = """
+    <option name="package" value="APPLICATION_ID" />
+
+""".trimIndent()
+
+private val BENCHMARK_PRESUBMIT_OPTION = """
+    <option name="instrumentation-arg" key="androidx.benchmark.dryRunMode.enable" value="true" />
+
+""".trimIndent()
+
+private val BENCHMARK_POSTSUBMIT_OPTIONS = """
+    <option name="instrumentation-arg" key="androidx.benchmark.output.enable" value="true" />
+    <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
+
+""".trimIndent()
+
+private val SMALL_TEST_OPTIONS = """
+    <option name="size" value="small" />
+    <option name="test-timeout" value="300" />
+
+""".trimIndent()
+
+private val MEDIUM_TEST_OPTIONS = """
+    <option name="size" value="medium" />
+    <option name="test-timeout" value="1500" />
+
+""".trimIndent()
+
+private val CLIENT_PREVIOUS = """
+    <option name="instrumentation-arg" key="client_version" value="previous" />
+
+""".trimIndent()
+
+private val CLIENT_TOT = """
+    <option name="instrumentation-arg" key="client_version" value="tot" />
+
+""".trimIndent()
+
+private val SERVICE_PREVIOUS = """
+    <option name="instrumentation-arg" key="service_version" value="previous" />
+
+""".trimIndent()
+
+private val SERVICE_TOT = """
+    <option name="instrumentation-arg" key="service_version" value="tot" />
+
+""".trimIndent()
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt
new file mode 100644
index 0000000..8ef93bcb
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.testConfiguration
+
+import androidx.build.dependencyTracker.ProjectSubset
+import androidx.build.renameApkForTesting
+import com.android.build.api.variant.BuiltArtifacts
+import com.android.build.api.variant.BuiltArtifactsLoader
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+/**
+ * Writes three configuration files to test combinations of media client & service in
+ * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
+ * format that gets zipped alongside the APKs to be tested. The combinations are of previous and
+ * tip-of-tree versions client and service. We want to test every possible pairing that includes
+ * tip-of-tree.
+ *
+ * This config gets ingested by Tradefed.
+ */
+abstract class GenerateMediaTestConfigurationTask : DefaultTask() {
+
+    @get:InputFiles
+    abstract val clientToTFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val clientToTLoader: Property<BuiltArtifactsLoader>
+
+    @get:InputFiles
+    abstract val clientPreviousFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val clientPreviousLoader: Property<BuiltArtifactsLoader>
+
+    @get:InputFiles
+    abstract val serviceToTFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val serviceToTLoader: Property<BuiltArtifactsLoader>
+
+    @get:InputFiles
+    abstract val servicePreviousFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val servicePreviousLoader: Property<BuiltArtifactsLoader>
+
+    @get:Input
+    abstract val affectedModuleDetectorSubset: Property<ProjectSubset>
+
+    @get:Input
+    abstract val clientToTPath: Property<String>
+
+    @get:Input
+    abstract val clientPreviousPath: Property<String>
+
+    @get:Input
+    abstract val serviceToTPath: Property<String>
+
+    @get:Input
+    abstract val servicePreviousPath: Property<String>
+
+    @get:Input
+    abstract val minSdk: Property<Int>
+
+    @get:Input
+    abstract val testRunner: Property<String>
+
+    @get:OutputFile
+    abstract val clientPreviousServiceToT: RegularFileProperty
+
+    @get:OutputFile
+    abstract val clientToTServicePrevious: RegularFileProperty
+
+    @get:OutputFile
+    abstract val clientToTServiceToT: RegularFileProperty
+
+    @TaskAction
+    fun generateAndroidTestZip() {
+        val clientToTApk = resolveApk(clientToTFolder, clientToTLoader)
+        val clientPreviousApk = resolveApk(clientPreviousFolder, clientPreviousLoader)
+        val serviceToTApk = resolveApk(serviceToTFolder, serviceToTLoader)
+        val servicePreviousApk = resolveApk(
+            servicePreviousFolder, servicePreviousLoader
+        )
+        writeConfigFileContent(
+            clientToTApk, serviceToTApk, clientToTPath.get(),
+            serviceToTPath.get(), clientToTServiceToT, false, false
+        )
+        writeConfigFileContent(
+            clientToTApk, servicePreviousApk, clientToTPath.get(),
+            servicePreviousPath.get(), clientToTServicePrevious, false, true
+        )
+        writeConfigFileContent(
+            clientPreviousApk, serviceToTApk, clientPreviousPath.get(),
+            serviceToTPath.get(), clientPreviousServiceToT, true, false
+        )
+    }
+
+    private fun resolveApk(
+        apkFolder: DirectoryProperty,
+        apkLoader: Property<BuiltArtifactsLoader>
+    ): BuiltArtifacts {
+        return apkLoader.get().load(apkFolder.get())
+            ?: throw RuntimeException("Cannot load required APK for task: $name")
+    }
+
+    private fun resolveName(apk: BuiltArtifacts, path: String): String {
+        return apk.elements.single().outputFile.substringAfterLast("/")
+            .renameApkForTesting(path, false)
+    }
+
+    private fun writeConfigFileContent(
+        clientApk: BuiltArtifacts,
+        serviceApk: BuiltArtifacts,
+        clientPath: String,
+        servicePath: String,
+        outputFile: RegularFileProperty,
+        isClientPrevious: Boolean,
+        isServicePrevious: Boolean
+    ) {
+        val configBuilder = MediaConfigBuilder()
+        configBuilder.clientApkName(resolveName(clientApk, clientPath))
+            .clientApplicationId(clientApk.applicationId)
+            .serviceApkName(resolveName(serviceApk, servicePath))
+            .serviceApplicationId(serviceApk.applicationId)
+            .minSdk(minSdk.get().toString())
+            .testRunner(testRunner.get())
+            .isClientPrevious(isClientPrevious)
+            .isServicePrevious(isServicePrevious)
+        when (affectedModuleDetectorSubset.get()) {
+            ProjectSubset.CHANGED_PROJECTS, ProjectSubset.ALL_AFFECTED_PROJECTS -> {
+                configBuilder.isPostsubmit(true)
+            }
+            ProjectSubset.DEPENDENT_PROJECTS -> {
+                configBuilder.isPostsubmit(false)
+            }
+            else -> {
+                throw IllegalStateException(
+                    "$name should not be running if the AffectedModuleDetector is returning " +
+                        "${affectedModuleDetectorSubset.get()} for this project."
+                )
+            }
+        }
+
+        val resolvedOutputFile: File = outputFile.asFile.get()
+        if (!resolvedOutputFile.exists()) {
+            if (!resolvedOutputFile.createNewFile()) {
+                throw RuntimeException(
+                    "Failed to create test configuration file: $outputFile"
+                )
+            }
+        }
+        resolvedOutputFile.writeText(configBuilder.build())
+    }
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
new file mode 100644
index 0000000..a95e43c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.testConfiguration
+
+import androidx.build.dependencyTracker.ProjectSubset
+import androidx.build.renameApkForTesting
+import com.android.build.api.variant.BuiltArtifactsLoader
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+/**
+ * Writes a configuration file in
+ * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
+ * format that gets zipped alongside the APKs to be tested.
+ * This config gets ingested by Tradefed.
+ */
+abstract class GenerateTestConfigurationTask : DefaultTask() {
+
+    @get:InputFiles
+    @get:Optional
+    abstract val appFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val appLoader: Property<BuiltArtifactsLoader>
+
+    @get:InputFiles
+    abstract val testFolder: DirectoryProperty
+
+    @get:Internal
+    abstract val testLoader: Property<BuiltArtifactsLoader>
+
+    @get:Input
+    abstract val minSdk: Property<Int>
+
+    @get:Input
+    abstract val hasBenchmarkPlugin: Property<Boolean>
+
+    @get:Input
+    abstract val testRunner: Property<String>
+
+    @get:Input
+    abstract val projectPath: Property<String>
+
+    @get:Input
+    abstract val affectedModuleDetectorSubset: Property<ProjectSubset>
+
+    @get:OutputFile
+    abstract val outputXml: RegularFileProperty
+
+    @TaskAction
+    fun generateAndroidTestZip() {
+        writeConfigFileContent()
+    }
+
+    private fun writeConfigFileContent() {
+        /*
+        Testing an Android Application project involves 2 APKS: an application to be instrumented,
+        and a test APK. Testing an Android Library project involves only 1 APK, since the library
+        is bundled inside the test APK, meaning it is self instrumenting. We add extra data to
+        configurations testing Android Application projects, so that both APKs get installed.
+         */
+        val configBuilder = ConfigBuilder()
+        if (appLoader.isPresent) {
+            val appApk = appLoader.get().load(appFolder.get())
+                ?: throw RuntimeException("Cannot load required APK for task: $name")
+            val appName = appApk.elements.single().outputFile.substringAfterLast("/")
+                .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
+            configBuilder.appApkName(appName)
+        }
+        val isPostsubmit: Boolean = when (affectedModuleDetectorSubset.get()) {
+            ProjectSubset.CHANGED_PROJECTS, ProjectSubset.ALL_AFFECTED_PROJECTS -> {
+                true
+            }
+            ProjectSubset.DEPENDENT_PROJECTS -> {
+                false
+            }
+            else -> {
+                throw IllegalStateException(
+                    "$name should not be running if the AffectedModuleDetector is returning " +
+                        "${affectedModuleDetectorSubset.get()} for this project."
+                )
+            }
+        }
+        configBuilder.isPostsubmit(isPostsubmit)
+        if (hasBenchmarkPlugin.get()) {
+            configBuilder.isBenchmark(true)
+            if (isPostsubmit) {
+                configBuilder.tag("microbenchmarks")
+            }
+        } else if (projectPath.get().endsWith("macrobenchmark")) {
+            configBuilder.tag("macrobenchmarks")
+        }
+        val testApk = testLoader.get().load(testFolder.get())
+            ?: throw RuntimeException("Cannot load required APK for task: $name")
+        val testName = testApk.elements.single().outputFile
+            .substringAfterLast("/")
+            .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
+        configBuilder.testApkName(testName)
+            .applicationId(testApk.applicationId)
+            .minSdk(minSdk.get().toString())
+            .testRunner(testRunner.get())
+
+        val resolvedOutputFile: File = outputXml.asFile.get()
+        if (!resolvedOutputFile.exists()) {
+            if (!resolvedOutputFile.createNewFile()) {
+                throw RuntimeException(
+                    "Failed to create test configuration file: $outputXml"
+                )
+            }
+        }
+        resolvedOutputFile.writeText(configBuilder.build())
+    }
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/TestSuiteConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
similarity index 92%
rename from buildSrc/src/main/kotlin/androidx/build/TestSuiteConfiguration.kt
rename to buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index c676f20..764abcc 100644
--- a/buildSrc/src/main/kotlin/androidx/build/TestSuiteConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -16,10 +16,15 @@
 
 @file:Suppress("UnstableApiUsage") // Incubating AGP APIs
 
-package androidx.build
+package androidx.build.testConfiguration
 
+import androidx.build.AndroidXPlugin
+import androidx.build.asFilenamePrefix
 import androidx.build.dependencyTracker.AffectedModuleDetector
+import androidx.build.getTestConfigDirectory
 import androidx.build.gradle.getByType
+import androidx.build.hasAndroidTestSourceCode
+import androidx.build.hasBenchmarkPlugin
 import com.android.build.api.artifact.ArtifactType
 import com.android.build.api.artifact.Artifacts
 import com.android.build.api.extension.AndroidComponentsExtension
@@ -85,8 +90,7 @@
         if (!project.parent!!.tasks.withType(GenerateMediaTestConfigurationTask::class.java)
             .names.contains(
                     "support-$mediaPrefix-test${
-                    AndroidXPlugin
-                        .GENERATE_TEST_CONFIGURATION_TASK
+                    AndroidXPlugin.GENERATE_TEST_CONFIGURATION_TASK
                     }"
                 )
         ) {
@@ -95,6 +99,11 @@
                 GenerateMediaTestConfigurationTask::class.java
             ) { task ->
                 AffectedModuleDetector.configureTaskGuard(task)
+                task.affectedModuleDetectorSubset.set(
+                    project.provider {
+                        AffectedModuleDetector.getProjectSubset(project)
+                    }
+                )
             }
             project.rootProject.tasks.findByName(AndroidXPlugin.ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
                 .dependsOn(task)
@@ -103,8 +112,7 @@
             return project.parent!!.tasks.withType(GenerateMediaTestConfigurationTask::class.java)
                 .named(
                     "support-$mediaPrefix-test${
-                    AndroidXPlugin
-                        .GENERATE_TEST_CONFIGURATION_TASK
+                    AndroidXPlugin.GENERATE_TEST_CONFIGURATION_TASK
                     }"
                 )
         }
@@ -203,4 +211,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index 5bfdf18..c92d780 100644
--- a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -47,8 +47,6 @@
     "createArchive",
     "createDiffArchiveForAll",
     "createProjectZip",
-    "desugarPublicDebugFileDependencies",
-    "desugarTipOfTreeDebugFileDependencies",
     "externalNativeBuildDebug",
     "externalNativeBuildRelease",
     "generateJsonModelDebug",
@@ -107,10 +105,7 @@
      */
     "relocateShadowJar",
     "stripArchiveForPartialDejetification",
-    "transformClassesWithDexBuilderForPublicDebug",
-    "transformClassesWithDexBuilderForTipOfTreeDebug",
     "verifyDependencyVersions",
-    "zipEcFiles",
     "zipTestConfigsWithApks",
 
     ":camera:integration-tests:camera-testapp-core:mergeLibDexDebug",
@@ -123,10 +118,6 @@
     ":camera:integration-tests:camera-testapp-view:GenerateTestConfigurationdebugAndroidTest",
     ":camera:integration-tests:camera-testapp-view:mergeLibDexDebug",
     ":camera:integration-tests:camera-testapp-view:packageDebug",
-
-    ":inspection:inspection-gradle-plugin:generatePomFileForInspectionPluginMarkerMavenPublication",
-    ":inspection:inspection-gradle-plugin:" +
-        "publishInspectionPluginMarkerMavenPublicationToMavenRepository"
 )
 
 // Additional tasks that are expected to be temporarily out-of-date after running once
diff --git a/busytown/androidx_max_dep_versions.sh b/busytown/androidx_max_dep_versions.sh
index 5dc8e64..aee0689 100755
--- a/busytown/androidx_max_dep_versions.sh
+++ b/busytown/androidx_max_dep_versions.sh
@@ -5,6 +5,8 @@
 
 cd "$(dirname $0)"
 
-impl/build.sh --no-daemon assembleDebug assembleAndroidTest -PuseMaxDepVersions --offline "$@"
+impl/build.sh --no-daemon --offline assembleDebug assembleAndroidTest \
+    -PuseMaxDepVersions \
+    -Pandroidx.validateNoUnrecognizedMessages "$@"
 
 echo "Completing $0 at $(date)"
diff --git a/busytown/androidx_test_changed_apks.sh b/busytown/androidx_test_changed_apks.sh
index 549692f..40a0bf44 100755
--- a/busytown/androidx_test_changed_apks.sh
+++ b/busytown/androidx_test_changed_apks.sh
@@ -9,6 +9,7 @@
     -Pandroidx.enableAffectedModuleDetection \
     -Pandroidx.changedProjects \
     -Pandroidx.coverageEnabled=true \
+    -Pandroidx.validateNoUnrecognizedMessages \
     -Pandroidx.allWarningsAsErrors --offline "$@"
 
 echo "Completing $0 at $(date)"
diff --git a/busytown/androidx_test_dependent_apks.sh b/busytown/androidx_test_dependent_apks.sh
index a2abc43..57745ba 100755
--- a/busytown/androidx_test_dependent_apks.sh
+++ b/busytown/androidx_test_dependent_apks.sh
@@ -9,6 +9,7 @@
     -Pandroidx.enableAffectedModuleDetection \
     -Pandroidx.dependentProjects \
     -Pandroidx.coverageEnabled=true \
+    -Pandroidx.validateNoUnrecognizedMessages \
     -Pandroidx.allWarningsAsErrors --offline "$@"
 
 echo "Completing $0 at $(date)"
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index 67bb30c..09fac0b 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -6,14 +6,18 @@
 # find script
 SCRIPT_DIR="$(cd $(dirname $0) && pwd)"
 
-# resolve DIST_DIR
+# resolve directories
 if [ "$DIST_DIR" == "" ]; then
   DIST_DIR="$SCRIPT_DIR/../../../../out/dist"
 fi
 mkdir -p "$DIST_DIR"
-
-# cd to checkout root
 cd "$SCRIPT_DIR/../../../.."
+OUT_DIR="$PWD/out"
+mkdir -p "$OUT_DIR"
+
+# record the build start time
+BUILD_START_MARKER="$OUT_DIR/build.sh.start"
+touch $BUILD_START_MARKER
 
 # runs a given command and prints its result if it fails
 function run() {
@@ -37,9 +41,12 @@
 fi
 # --no-watch-fs disables file system watch, because it does not work on busytown
 # due to our builders using OS that is too old.
-run $PROJECTS_ARG OUT_DIR=out DIST_DIR=$DIST_DIR ANDROID_HOME=./prebuilts/fullsdk-linux \
+run $PROJECTS_ARG OUT_DIR=$OUT_DIR DIST_DIR=$DIST_DIR ANDROID_HOME=./prebuilts/fullsdk-linux \
     frameworks/support/gradlew -p frameworks/support \
     --stacktrace \
     -Pandroidx.summarizeStderr \
     --no-watch-fs \
     "$@"
+
+# check that no unexpected modifications were made to the source repository, such as new cache directories
+$SCRIPT_DIR/verify_no_caches_in_source_repo.sh $BUILD_START_MARKER
diff --git a/busytown/impl/verify_no_caches_in_source_repo.sh b/busytown/impl/verify_no_caches_in_source_repo.sh
new file mode 100755
index 0000000..7e5e028
--- /dev/null
+++ b/busytown/impl/verify_no_caches_in_source_repo.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+set -e
+
+function usage() {
+  echo "Confirms that no unexpected, generated files exist in the source repository"
+  echo
+  echo "Usage: $0 <timestamp file>"
+  echo
+  echo "<timestamp file>: any file newer than this one will be considered an error unless it is already exempted"
+  return 1
+}
+
+# parse arguments
+# a file whose timestamp is the oldest acceptable timestamp for source files
+COMPARE_TO_FILE="$1"
+if [ "$COMPARE_TO_FILE" == "" ]; then
+  usage
+fi
+
+# get script path
+SCRIPT_DIR="$(cd $(dirname $0) && pwd)"
+SOURCE_DIR="$(cd $SCRIPT_DIR/../.. && pwd)"
+
+# confirm that no files in the source repo were unexpectedly created (other than known exemptions)
+function checkForGeneratedFilesInSourceRepo() {
+
+  # Paths that are still expected to be generated and that we have to allow
+  # If you need add or remove an exemption here, update cleanBuild.sh too
+  EXEMPT_PATHS=".gradle appsearch/local-storage/.cxx buildSrc/.gradle local.properties"
+  # put "./" in front of each path to match the output from 'find'
+  EXEMPT_PATHS="$(echo " $EXEMPT_PATHS" | sed 's| | ./|g')"
+  # build a `find` argument for skipping descending into the exempt paths
+  EXEMPTIONS_ARGUMENT="$(echo $EXEMPT_PATHS | sed 's/ /\n/g' | sed 's|\(.*\)|-path \1 -prune -o|g' | xargs echo)"
+
+  # Search for files that were created or updated more recently than the build start.
+  # Unfortunately we can't also include directories because the `test` task seems to update
+  # the modification time in several projects
+  GENERATED_FILES="$(cd $SOURCE_DIR && find . $EXEMPTIONS_ARGUMENT -newer $COMPARE_TO_FILE -type f)"
+  UNEXPECTED_GENERATED_FILES=""
+  for f in $GENERATED_FILES; do
+    exempt=false
+    for exemption in $EXEMPT_PATHS; do
+      if [ "$f" == "$exemption" ]; then
+        exempt=true
+        break
+      fi
+      if [ "$f" == "$(dirname $exemption)" ]; then
+        # When the exempt directory gets created, its parent dir will be modified
+        # So, we ignore changes to the parent dir too (but not necessarily changes in sibling dirs)
+        exempt=true
+        break
+      fi
+    done
+    if [ "$exempt" == "false" ]; then
+      UNEXPECTED_GENERATED_FILES="$UNEXPECTED_GENERATED_FILES $f"
+    fi
+  done
+  if [ "$UNEXPECTED_GENERATED_FILES" != "" ]; then
+    echo >&2
+    echo "Unexpectedly found these files generated or modified by the build:
+
+${UNEXPECTED_GENERATED_FILES}
+
+Generated files should go in OUT_DIR instead because that is where developers expect to find them
+(to make it easier to diagnose build problems: inspect or delete these files)" >&2
+    exit 1
+  fi
+}
+checkForGeneratedFilesInSourceRepo
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index fe1548c..30e18b6 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -18,6 +18,7 @@
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 import static androidx.build.dependencies.DependenciesKt.*
 
@@ -25,6 +26,7 @@
     id("AndroidXPlugin")
     id("com.android.library")
     id("kotlin-android")
+    id("kotlin-kapt")
 }
 
 apply from: "../camera-camera2-pipe/dependencies.gradle"
@@ -39,14 +41,21 @@
     releaseBundleInside(project(path: ':camera:camera-camera2-pipe', configuration: "exportRelease"))
     debugBundleInside(project(path: ':camera:camera-camera2-pipe', configuration: "exportDebug"))
 
+    // Classes and types that are needed at compile & runtime
     api("androidx.annotation:annotation:1.1.0")
     api(project(":camera:camera-core"))
+
+    // Classes and types that are only needed at runtime
+    implementation(project(":lifecycle:lifecycle-livedata-ktx"))
+    implementation(KOTLIN_COROUTINES_GUAVA)
     implementation(KOTLIN_STDLIB)
 
     // Since we jarjar CameraPipe, include the transitive dependencies as implementation
     implementation CAMERA_PIPE_DEPS.API
     implementation CAMERA_PIPE_DEPS.IMPLEMENTATION
 
+    kapt(DAGGER_COMPILER)
+
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(KOTLIN_COROUTINES_ANDROID)
@@ -63,6 +72,20 @@
     }
 }
 
+kapt {
+    javacOptions {
+        option("-Adagger.fastInit=enabled")
+        option("-Adagger.fullBindingGraphValidation=ERROR")
+    }
+}
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
+    }
+}
+
 androidx {
     name = "Jetpack Camera Camera Pipe Integration Library"
     publish = Publish.NONE
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
index 473559c..bb925bd 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
@@ -16,9 +16,9 @@
 package androidx.camera.camera2.pipe.integration
 
 import androidx.camera.core.CameraXConfig
-import androidx.camera.camera2.pipe.integration.impl.CameraPipeFactory
-import androidx.camera.camera2.pipe.integration.impl.StreamConfigurationMap
-import androidx.camera.camera2.pipe.integration.impl.UseCaseConfigurationMap
+import androidx.camera.camera2.pipe.integration.adapter.CameraFactoryAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraSurfaceAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter
 
 /**
  * Convenience class for generating a pre-populated CameraPipe based [CameraXConfig].
@@ -29,9 +29,9 @@
      */
     fun defaultConfig(): CameraXConfig {
         return CameraXConfig.Builder()
-            .setCameraFactoryProvider(::CameraPipeFactory)
-            .setDeviceSurfaceManagerProvider(::StreamConfigurationMap)
-            .setUseCaseConfigFactoryProvider(::UseCaseConfigurationMap)
+            .setCameraFactoryProvider(::CameraFactoryAdapter)
+            .setDeviceSurfaceManagerProvider(::CameraSurfaceAdapter)
+            .setUseCaseConfigFactoryProvider(::CameraUseCaseAdapter)
             .build()
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
new file mode 100644
index 0000000..dcf14ed5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.CameraState
+import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.FocusMeteringResult
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.TorchState
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.utils.futures.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Adapt the [CameraControlInternal] interface to [CameraPipe].
+ *
+ * This controller class maintains state as use-cases are attached / detached from the camera as
+ * well as providing access to other utility methods. The primary purpose of this class it to
+ * forward these interactions to the currently configured [UseCaseCamera].
+ */
+@CameraScope
+@OptIn(ExperimentalCoroutinesApi::class)
+class CameraControlAdapter @Inject constructor(
+    lazyCameraMetadata: Provider<CameraMetadata>,
+    private val cameraScope: CoroutineScope,
+    private val cameraState: CameraState,
+    private val useCaseManager: UseCaseManager
+) : CameraControlInternal {
+    private val cameraMetadata by lazy { lazyCameraMetadata.get() }
+    private var interopConfig: Config = MutableOptionsBundle.create()
+    private var imageCaptureFlashMode: Int = ImageCapture.FLASH_MODE_OFF
+
+    override fun getSensorRect(): Rect {
+        return cameraMetadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]!!
+    }
+
+    override fun addInteropConfig(config: Config) {
+        interopConfig = Config.mergeConfigs(config, interopConfig)
+    }
+
+    override fun clearInteropConfig() {
+        interopConfig = MutableOptionsBundle.create()
+    }
+
+    override fun getInteropConfig(): Config {
+        return interopConfig
+    }
+
+    override fun enableTorch(torch: Boolean): ListenableFuture<Void> {
+        // Launch UNDISPATCHED to preserve interaction order with the camera.
+        return cameraScope.launchAsVoidFuture(start = CoroutineStart.UNDISPATCHED) {
+            useCaseManager.camera?.let {
+                // Tell the camera to turn the torch on / off.
+                val result = it.enableTorchAsync(torch)
+
+                // Update the torch state.
+                withContext(Dispatchers.Main) {
+                    cameraState.torchState.value = when (torch) {
+                        true -> TorchState.ON
+                        false -> TorchState.OFF
+                    }
+                }
+
+                // Wait until the command is received by the camera.
+                result.await()
+            }
+        }
+    }
+
+    override fun startFocusAndMetering(
+        action: FocusMeteringAction
+    ): ListenableFuture<FocusMeteringResult> {
+        warn { "TODO: startFocusAndMetering is not yet supported" }
+        return Futures.immediateFuture(FocusMeteringResult.emptyInstance())
+    }
+
+    override fun cancelFocusAndMetering(): ListenableFuture<Void> {
+        warn { "TODO: cancelFocusAndMetering is not yet supported" }
+        return Futures.immediateFuture(null)
+    }
+
+    override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
+        // Note: The current implementation waits until the update has been *submitted* to the
+        //   camera, but does not wait for the total capture result.
+        warn { "TODO: setZoomRatio is not yet supported" }
+        return Futures.immediateFuture(null)
+    }
+
+    override fun setLinearZoom(linearZoom: Float): ListenableFuture<Void> {
+        warn { "TODO: setLinearZoom is not yet supported" }
+        return Futures.immediateFuture(null)
+    }
+
+    override fun getFlashMode(): Int {
+        return imageCaptureFlashMode
+    }
+
+    override fun setFlashMode(flashMode: Int) {
+        warn { "TODO: setFlashMode is not yet supported" }
+        this.imageCaptureFlashMode = flashMode
+    }
+
+    override fun triggerAf(): ListenableFuture<CameraCaptureResult> {
+        warn { "TODO: triggerAf is not yet supported" }
+        return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
+    }
+
+    override fun triggerAePrecapture(): ListenableFuture<CameraCaptureResult> {
+        warn { "TODO: triggerAePrecapture is not yet supported" }
+        return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
+    }
+
+    override fun cancelAfAeTrigger(cancelAfTrigger: Boolean, cancelAePrecaptureTrigger: Boolean) {
+        warn { "TODO: cancelAfAeTrigger is not yet supported" }
+    }
+
+    @SuppressLint("UnsafeExperimentalUsageError")
+    override fun setExposureCompensationIndex(exposure: Int): ListenableFuture<Int> {
+        warn { "TODO: setExposureCompensationIndex is not yet supported" }
+        return Futures.immediateFuture(exposure)
+    }
+
+    override fun submitCaptureRequests(captureConfigs: MutableList<CaptureConfig>) {
+        warn { "TODO: submitCaptureRequests is not yet supported" }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
new file mode 100644
index 0000000..120f8b9
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.content.Context
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.impl.Debug
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Timestamps
+import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
+import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.camera2.pipe.integration.config.CameraAppConfig
+import androidx.camera.camera2.pipe.integration.config.DaggerCameraAppComponent
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.CameraThreadConfig
+
+/**
+ * The [CameraFactoryAdapter] is responsible for creating the root dagger component that is used
+ * to share resources across Camera instances.
+ */
+class CameraFactoryAdapter(
+    context: Context,
+    threadConfig: CameraThreadConfig,
+    availableCamerasSelector: CameraSelector?
+) : CameraFactory {
+    private val appComponent: CameraAppComponent by lazy {
+        Debug.traceStart { "CameraFactoryAdapter#appComponent" }
+        val start = Timestamps.now()
+        val result = DaggerCameraAppComponent.builder()
+            .config(CameraAppConfig(context, threadConfig))
+            .build()
+        debug { "Created CameraFactoryAdapter in ${start.measureNow().formatMs()}" }
+        debug { "availableCamerasSelector: $availableCamerasSelector " }
+        Debug.traceStop()
+        result
+    }
+
+    init {
+        debug { "Created CameraFactoryAdapter" }
+    }
+
+    override fun getCamera(cameraId: String): CameraInternal =
+        appComponent.cameraBuilder()
+            .config(CameraConfig(CameraId(cameraId)))
+            .build()
+            .getCameraInternal()
+
+    override fun getAvailableCameraIds(): Set<String> = appComponent.getAvailableCameraIds()
+    override fun getCameraManager(): Any? = appComponent
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
new file mode 100644
index 0000000..1ad9830
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
+import androidx.camera.camera2.pipe.integration.impl.CameraState
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ExposureState
+import androidx.camera.core.ZoomState
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.utils.CameraOrientationUtil
+import androidx.lifecycle.LiveData
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Provider
+
+internal val defaultQuirks = Quirks(emptyList())
+
+/**
+ * Adapt the [CameraInfoInternal] interface to [CameraPipe].
+ */
+@SuppressLint(
+    "UnsafeExperimentalUsageError" // Suppressed due to experimental ExposureState
+)
+@CameraScope
+class CameraInfoAdapter @Inject constructor(
+    private val lazyCameraMetadata: Provider<CameraMetadata>,
+    private val cameraConfig: CameraConfig,
+    private val cameraState: CameraState,
+    private val cameraCallbackMap: CameraCallbackMap
+) : CameraInfoInternal {
+
+    private val cameraMetadata: CameraMetadata
+        get() = lazyCameraMetadata.get()
+
+    override fun getCameraId(): String = cameraConfig.cameraId.value
+    override fun getLensFacing(): Int? = cameraMetadata[CameraCharacteristics.LENS_FACING]
+    override fun getSensorRotationDegrees(): Int = getSensorRotationDegrees(Surface.ROTATION_0)
+    override fun hasFlashUnit(): Boolean =
+        cameraMetadata[CameraCharacteristics.FLASH_INFO_AVAILABLE]!!
+
+    override fun getSensorRotationDegrees(relativeRotation: Int): Int {
+        val sensorOrientation: Int = cameraMetadata[CameraCharacteristics.SENSOR_ORIENTATION]!!
+        val relativeRotationDegrees =
+            CameraOrientationUtil.surfaceRotationToDegrees(relativeRotation)
+        // Currently this assumes that a back-facing camera is always opposite to the screen.
+        // This may not be the case for all devices, so in the future we may need to handle that
+        // scenario.
+        val lensFacing = lensFacing
+        val isOppositeFacingScreen =
+            lensFacing != null && CameraSelector.LENS_FACING_BACK == lensFacing
+        return CameraOrientationUtil.getRelativeImageRotation(
+            relativeRotationDegrees,
+            sensorOrientation,
+            isOppositeFacingScreen
+        )
+    }
+
+    override fun getZoomState(): LiveData<ZoomState> = cameraState.zoomState
+    override fun getTorchState(): LiveData<Int> = cameraState.torchState
+    @SuppressLint("UnsafeExperimentalUsageError")
+    override fun getExposureState(): ExposureState = cameraState.exposureState.value!!
+
+    override fun addSessionCaptureCallback(executor: Executor, callback: CameraCaptureCallback) =
+        cameraCallbackMap.addCaptureCallback(callback, executor)
+    override fun removeSessionCaptureCallback(callback: CameraCaptureCallback) =
+        cameraCallbackMap.removeCaptureCallback(callback)
+
+    override fun getImplementationType(): String = "CameraPipe"
+    override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
+
+    override fun getCameraQuirks(): Quirks {
+        Log.warn { "TODO: Quirks are not yet supported." }
+        return defaultQuirks
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
new file mode 100644
index 0000000..d3dcff7
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.LiveDataObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.utils.futures.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.atomicfu.atomic
+import javax.inject.Inject
+
+internal val cameraAdapterIds = atomic(0)
+
+/**
+ * Adapt the [CameraInternal] class to one or more [CameraPipe] based Camera instances.
+ */
+@CameraScope
+class CameraInternalAdapter @Inject constructor(
+    config: CameraConfig,
+    private val useCaseManager: UseCaseManager,
+    private val cameraInfo: CameraInfoInternal,
+    private val cameraController: CameraControlInternal
+) : CameraInternal {
+    private val cameraId = config.cameraId
+    private val debugId = cameraAdapterIds.incrementAndGet()
+    private val cameraState = LiveDataObservable<CameraInternal.State>()
+
+    init {
+        cameraState.postValue(CameraInternal.State.CLOSED)
+
+        debug { "Created $this for $cameraId" }
+        // TODO: Consider preloading the list of camera ids and metadata.
+    }
+
+    // Load / unload methods
+    override fun open() {
+        debug { "$this#open" }
+    }
+
+    override fun close() {
+        debug { "$this#close" }
+    }
+
+    override fun release(): ListenableFuture<Void> {
+        warn { "$this#release is not yet implemented." }
+        // TODO: Determine what the correct way to invoke release is.
+        return Futures.immediateFuture(null)
+    }
+
+    override fun getCameraInfoInternal(): CameraInfoInternal = cameraInfo
+    override fun getCameraState(): Observable<CameraInternal.State> = cameraState
+    override fun getCameraControlInternal(): CameraControlInternal = cameraController
+
+    // UseCase attach / detach behaviors.
+    override fun attachUseCases(useCasesToAdd: MutableCollection<UseCase>) {
+        useCaseManager.attach(useCasesToAdd.toList())
+    }
+
+    override fun detachUseCases(useCasesToRemove: MutableCollection<UseCase>) {
+        useCaseManager.detach(useCasesToRemove.toList())
+    }
+
+    // UseCase state callbacks
+    override fun onUseCaseActive(useCase: UseCase) {
+        useCaseManager.enable(useCase)
+    }
+
+    override fun onUseCaseUpdated(useCase: UseCase) {
+        useCaseManager.update(useCase)
+    }
+
+    override fun onUseCaseReset(useCase: UseCase) {
+        useCaseManager.update(useCase)
+    }
+
+    override fun onUseCaseInactive(useCase: UseCase) {
+        useCaseManager.disable(useCase)
+    }
+
+    override fun toString(): String = "CameraInternalAdapter<$cameraId>"
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
new file mode 100644
index 0000000..d026e65
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.SurfaceConfig
+import androidx.camera.core.impl.SurfaceConfig.ConfigSize
+import androidx.camera.core.impl.UseCaseConfig
+
+internal val MAXIMUM_PREVIEW_SIZE = Size(1920, 1080)
+
+/**
+ * Adapt the [CameraDeviceSurfaceManager] interface to [CameraPipe].
+ *
+ * This class provides Context-specific utility methods for querying and computing supported
+ * outputs.
+ */
+class CameraSurfaceAdapter(
+    context: Context,
+    cameraComponent: Any?,
+    availableCameraIds: Set<String>
+) : CameraDeviceSurfaceManager {
+    private val component = cameraComponent as CameraAppComponent
+
+    init {
+        debug { "AvailableCameraIds = $availableCameraIds" }
+        debug { "Created StreamConfigurationMap from $context" }
+    }
+
+    override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
+        // TODO: This method needs to check to see if the list of SurfaceConfig's is in the map of
+        //   guaranteed stream configurations for this camera's support level.
+        return component.getAvailableCameraIds().contains(cameraId)
+    }
+
+    override fun transformSurfaceConfig(
+        cameraId: String,
+        imageFormat: Int,
+        size: Size
+    ): SurfaceConfig? {
+        // TODO: Many of the "find a stream combination that will work" is already provided by the
+        //   existing camera2 implementation, and this implementation should leverage that work.
+
+        val configType = when (imageFormat) {
+            ImageFormat.YUV_420_888 -> SurfaceConfig.ConfigType.YUV
+            ImageFormat.JPEG -> SurfaceConfig.ConfigType.JPEG
+            ImageFormat.RAW_SENSOR -> SurfaceConfig.ConfigType.RAW
+            else -> SurfaceConfig.ConfigType.PRIV
+        }
+
+        val configSize = ConfigSize.PREVIEW
+        return SurfaceConfig.create(configType, configSize)
+    }
+
+    override fun getSuggestedResolutions(
+        cameraId: String,
+        existingSurfaces: List<SurfaceConfig>,
+        newUseCaseConfigs: List<UseCaseConfig<*>?>
+    ): Map<UseCaseConfig<*>, Size> {
+        // TODO: Many of the "find a stream combination that will work" is already provided by the
+        //   existing camera2 implementation, and this implementation should leverage that work.
+
+        val sizes: MutableMap<UseCaseConfig<*>, Size> = mutableMapOf()
+        for (config in newUseCaseConfigs) {
+            sizes[config as UseCaseConfig<*>] = MAXIMUM_PREVIEW_SIZE
+        }
+        return sizes
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
new file mode 100644
index 0000000..dcbcba5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.camera2.CameraDevice
+import android.util.Size
+import android.view.Display
+import android.view.WindowManager
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Log.info
+import androidx.camera.camera2.pipe.integration.impl.asLandscape
+import androidx.camera.camera2.pipe.integration.impl.minByArea
+import androidx.camera.camera2.pipe.integration.impl.toSize
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory
+
+/**
+ * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
+ *
+ * This includes things like default template and session parameters, as well as maximum resolution
+ * and aspect ratios for the display.
+ */
+class CameraUseCaseAdapter(context: Context) : UseCaseConfigFactory {
+
+    private val display: Display by lazy {
+        @Suppress("deprecation")
+        (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay!!
+    }
+
+    init {
+        if (context === context.applicationContext) {
+            info {
+                "The provided context ($context) is application scoped and will be used to infer " +
+                    "the default display for computing the default preview size, orientation, " +
+                    "and default aspect ratio for UseCase outputs."
+            }
+        }
+        debug { "Created UseCaseConfigurationMap" }
+    }
+
+    /**
+     * Returns the configuration for the given capture type, or `null` if the
+     * configuration cannot be produced.
+     */
+    override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
+        debug { "Creating config for $captureType" }
+
+        val mutableConfig = MutableOptionsBundle.create()
+        val sessionBuilder = SessionConfig.Builder()
+        // TODO(b/114762170): Must set to preview here until we allow for multiple template
+        //  types
+        sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG,
+            sessionBuilder.build()
+        )
+        val captureBuilder = CaptureConfig.Builder()
+        when (captureType) {
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE ->
+                captureBuilder.templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS,
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE ->
+                captureBuilder.templateType = CameraDevice.TEMPLATE_PREVIEW
+        }
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG,
+            captureBuilder.build()
+        )
+
+        // Only CAPTURE_TYPE_IMAGE_CAPTURE has its own ImageCaptureOptionUnpacker. Other
+        // capture types all use the standard Camera2CaptureOptionUnpacker.
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER,
+            DefaultCaptureOptionsUnpacker
+        )
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER,
+            DefaultSessionOptionsUnpacker
+        )
+
+        if (captureType == UseCaseConfigFactory.CaptureType.PREVIEW) {
+            mutableConfig.insertOption(
+                ImageOutputConfig.OPTION_MAX_RESOLUTION,
+                getPreviewSize()
+            )
+        }
+
+        mutableConfig.insertOption(
+            ImageOutputConfig.OPTION_TARGET_ROTATION,
+            display.rotation
+        )
+        return OptionsBundle.from(mutableConfig)
+    }
+
+    /**
+     * Returns the device's screen resolution, or 1080p, whichever is smaller.
+     */
+    private fun getPreviewSize(): Size? {
+        val displaySize = Point()
+        display.getRealSize(displaySize)
+        return minByArea(MAXIMUM_PREVIEW_SIZE, displaySize.toSize().asLandscape())
+    }
+
+    object DefaultCaptureOptionsUnpacker : CaptureConfig.OptionUnpacker {
+        override fun unpack(config: UseCaseConfig<*>, builder: CaptureConfig.Builder) {
+            // Unused.
+        }
+    }
+
+    object DefaultSessionOptionsUnpacker : SessionConfig.OptionUnpacker {
+        override fun unpack(config: UseCaseConfig<*>, builder: SessionConfig.Builder) {
+            // Unused.
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
new file mode 100644
index 0000000..63f471f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.hardware.camera2.CaptureResult
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.integration.impl.CAMERAX_TAG_BUNDLE
+import androidx.camera.core.impl.CameraCaptureMetaData.AeState
+import androidx.camera.core.impl.CameraCaptureMetaData.AfMode
+import androidx.camera.core.impl.CameraCaptureMetaData.AfState
+import androidx.camera.core.impl.CameraCaptureMetaData.AwbState
+import androidx.camera.core.impl.CameraCaptureMetaData.FlashState
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.TagBundle
+
+/**
+ * Adapts the [CameraCaptureResult] interface to [CameraPipe].
+ */
+class CaptureResultAdapter(
+    private val requestMetadata: RequestMetadata,
+    private val frameNumber: FrameNumber,
+    private val result: FrameInfo
+) : CameraCaptureResult {
+    override fun getAfMode(): AfMode =
+        when (val mode = result.metadata[CaptureResult.CONTROL_AF_MODE]) {
+            CaptureResult.CONTROL_AF_MODE_OFF,
+            CaptureResult.CONTROL_AF_MODE_EDOF -> AfMode.OFF
+            CaptureResult.CONTROL_AF_MODE_AUTO,
+            CaptureResult.CONTROL_AF_MODE_MACRO -> AfMode.ON_MANUAL_AUTO
+            CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+            CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO -> AfMode.ON_CONTINUOUS_AUTO
+            null -> AfMode.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AF mode ($mode) for $frameNumber!" }
+                AfMode.UNKNOWN
+            }
+        }
+
+    override fun getAfState(): AfState =
+        when (val state = result.metadata[CaptureResult.CONTROL_AF_STATE]) {
+            CaptureResult.CONTROL_AF_STATE_INACTIVE -> AfState.INACTIVE
+            CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> AfState.SCANNING
+            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> AfState.LOCKED_FOCUSED
+            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> AfState.LOCKED_NOT_FOCUSED
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> AfState.FOCUSED
+            null -> AfState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AF state ($state) for $frameNumber!" }
+                AfState.UNKNOWN
+            }
+        }
+
+    override fun getAeState(): AeState =
+        when (val state = result.metadata[CaptureResult.CONTROL_AE_STATE]) {
+            CaptureResult.CONTROL_AE_STATE_INACTIVE -> AeState.INACTIVE
+            CaptureResult.CONTROL_AE_STATE_SEARCHING,
+            CaptureResult.CONTROL_AE_STATE_PRECAPTURE -> AeState.SEARCHING
+            CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> AeState.FLASH_REQUIRED
+            CaptureResult.CONTROL_AE_STATE_CONVERGED -> AeState.CONVERGED
+            CaptureResult.CONTROL_AE_STATE_LOCKED -> AeState.LOCKED
+            null -> AeState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AE state ($state) for $frameNumber!" }
+                AeState.UNKNOWN
+            }
+        }
+
+    override fun getAwbState(): AwbState =
+        when (val state = result.metadata[CaptureResult.CONTROL_AWB_STATE]) {
+            CaptureResult.CONTROL_AWB_STATE_INACTIVE -> AwbState.INACTIVE
+            CaptureResult.CONTROL_AWB_STATE_SEARCHING -> AwbState.METERING
+            CaptureResult.CONTROL_AWB_STATE_CONVERGED -> AwbState.CONVERGED
+            CaptureResult.CONTROL_AWB_STATE_LOCKED -> AwbState.LOCKED
+            null -> AwbState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AWB state ($state) for $frameNumber!" }
+                AwbState.UNKNOWN
+            }
+        }
+
+    override fun getFlashState(): FlashState =
+        when (val state = result.metadata[CaptureResult.FLASH_STATE]) {
+            CaptureResult.FLASH_STATE_UNAVAILABLE,
+            CaptureResult.FLASH_STATE_CHARGING -> FlashState.NONE
+            CaptureResult.FLASH_STATE_READY -> FlashState.READY
+            CaptureResult.FLASH_STATE_FIRED,
+            CaptureResult.FLASH_STATE_PARTIAL -> FlashState.FIRED
+            null -> FlashState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown flash state ($state) for $frameNumber!" }
+                FlashState.UNKNOWN
+            }
+        }
+
+    override fun getTimestamp(): Long {
+        return result.metadata.getOrDefault(CaptureResult.SENSOR_TIMESTAMP, -1L)
+    }
+
+    override fun getTagBundle(): TagBundle {
+        return requestMetadata.getOrDefault(CAMERAX_TAG_BUNDLE, TagBundle.emptyBundle())
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
new file mode 100644
index 0000000..96c3dc8
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.async
+import kotlinx.coroutines.guava.asListenableFuture
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * This allows a java method that returns a ListenableFuture<Void> to be implemented by calling a
+ * suspend function.
+ *
+ * Exceptions thrown from the coroutine scope are propagated to the returned future.
+ * Canceling the future will attempt to cancel the coroutine.
+ */
+fun CoroutineScope.launchAsVoidFuture(
+    context: CoroutineContext = EmptyCoroutineContext,
+    start: CoroutineStart = CoroutineStart.DEFAULT,
+    block: suspend CoroutineScope.() -> Unit
+): ListenableFuture<Void> {
+    // TODO: This method currently uses guava.asListenableFuture. This may be an expensive
+    //  dependency to take on. We may need to evaluate this.
+    @Suppress("UNCHECKED_CAST")
+    return this.async(context = context, start = start, block = block)
+        .asListenableFuture() as ListenableFuture<Void>
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
new file mode 100644
index 0000000..bb05d8d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.util.Range
+import android.util.Rational
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.ExposureState
+
+internal val EMPTY_RANGE = Range(0, 0)
+
+/** Adapt [ExposureState] to a [CameraMetadata] instance. */
+@SuppressLint("UnsafeExperimentalUsageError")
+class ExposureStateAdapter(
+    private val cameraMetadata: CameraMetadata,
+    private val exposureCompensation: Int
+) : ExposureState {
+    override fun isExposureCompensationSupported(): Boolean {
+        val range = exposureCompensationRange
+        return range.lower != 0 && range.upper != 0
+    }
+
+    override fun getExposureCompensationIndex(): Int = exposureCompensation
+    override fun getExposureCompensationStep(): Rational {
+        if (!isExposureCompensationSupported) {
+            return Rational.ZERO
+        }
+        return cameraMetadata[CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP]!!
+    }
+
+    override fun getExposureCompensationRange(): Range<Int> {
+        return cameraMetadata[CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE]
+            ?: EMPTY_RANGE
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt
new file mode 100644
index 0000000..4d42273
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.ZoomState
+
+/**
+ * Adapt [ZoomState] to a [CameraMetadata] instance.
+ */
+@SuppressLint("UnsafeExperimentalUsageError")
+class ZoomStateAdapter(
+    cameraMetadata: CameraMetadata,
+    private val zoomRatio: Float
+) : ZoomState {
+    // TODO: Zoom state has API-specific compat requirements. This is a placeholder for newer
+    //  android API versions and will require compat changes to support older versions.
+    private val zoomRange = cameraMetadata[CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE]!!
+
+    override fun getMaxZoomRatio(): Float = zoomRange.upper as Float
+    override fun getMinZoomRatio(): Float = zoomRange.lower as Float
+    override fun getLinearZoom(): Float {
+        val range = zoomRange.upper - zoomRange.lower
+        if (range > 0) {
+            return (zoomRatio - zoomRange.lower) / range
+        }
+        return 1.0f
+    }
+    override fun getZoomRatio(): Float = zoomRatio
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
similarity index 78%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
index f9cb2fe..f7a9ea2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
@@ -14,7 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.camera2.pipe.integration.adapter;
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
new file mode 100644
index 0000000..d8eadf0
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.config
+
+import android.content.Context
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.core.impl.CameraThreadConfig
+import androidx.camera.core.impl.CameraFactory
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/** Dependency bindings for adapting a [CameraFactory] instance to [CameraPipe] */
+@Module(
+    subcomponents = [CameraComponent::class]
+)
+abstract class CameraAppModule {
+    companion object {
+        @Singleton
+        @Provides
+        fun provideCameraPipe(context: Context): CameraPipe {
+            return CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
+        }
+
+        @Provides
+        fun provideAvailableCameraIds(cameraPipe: CameraPipe): Set<String> {
+            return cameraPipe.cameras().findAll().map { it.value }.toSet()
+        }
+    }
+}
+
+/** Configuration properties that are shared across this app process */
+@Module
+class CameraAppConfig(
+    private val context: Context,
+    private val threadConfig: CameraThreadConfig
+) {
+    @Provides
+    fun provideContext(): Context = context
+}
+
+/** Dagger component for Application (Process) scoped dependencies. */
+@Singleton
+@Component(
+    modules = [
+        CameraAppModule::class,
+        CameraAppConfig::class
+    ]
+)
+interface CameraAppComponent {
+    fun cameraBuilder(): CameraComponent.Builder
+    fun getAvailableCameraIds(): Set<String>
+
+    @Component.Builder
+    interface Builder {
+        fun config(config: CameraAppConfig): Builder
+        fun build(): CameraAppComponent
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
new file mode 100644
index 0000000..803d605
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.config
+
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraInternalAdapter
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.CameraInternal
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import javax.inject.Scope
+
+@Scope
+annotation class CameraScope
+
+/** Dependency bindings for adapting an individual [CameraInternal] instance to [CameraPipe] */
+@Module(
+    subcomponents = [UseCaseCameraComponent::class]
+)
+abstract class CameraModule {
+    companion object {
+        @CameraScope
+        @Provides
+        fun provideCameraCoroutineScope(cameraConfig: CameraConfig): CoroutineScope {
+            // TODO: Dispatchers.Default is the standard kotlin coroutine executor for background
+            //   work, but we may want to pass something in.
+            return CoroutineScope(
+                Job() +
+                    Dispatchers.Default +
+                    CoroutineName("CXCP-Camera-${cameraConfig.cameraId.value}")
+            )
+        }
+
+        @Provides
+        fun provideCameraMetadata(cameraPipe: CameraPipe, config: CameraConfig): CameraMetadata =
+            cameraPipe.cameras().awaitMetadata(config.cameraId)
+    }
+
+    @Binds
+    abstract fun bindCameraInternal(adapter: CameraInternalAdapter): CameraInternal
+
+    @Binds
+    abstract fun bindCameraInfoInternal(adapter: CameraInfoAdapter): CameraInfoInternal
+
+    @Binds
+    abstract fun bindCameraControlInternal(adapter: CameraControlAdapter): CameraControlInternal
+}
+
+/** Configuration properties used when creating a [CameraInternal] instance. */
+@Module
+class CameraConfig(val cameraId: CameraId) {
+    @Provides
+    fun provideCameraConfig(): CameraConfig = this
+}
+
+/** Dagger subcomponent for a single [CameraInternal] instance. */
+@CameraScope
+@Subcomponent(
+    modules = [
+        CameraModule::class,
+        CameraConfig::class
+    ]
+)
+interface CameraComponent {
+    @Subcomponent.Builder
+    interface Builder {
+        fun config(config: CameraConfig): Builder
+        fun build(): CameraComponent
+    }
+
+    fun getCameraInternal(): CameraInternal
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
new file mode 100644
index 0000000..8ea76cf
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.config
+
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.core.UseCase
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import javax.inject.Scope
+
+@Scope
+annotation class UseCaseCameraScope
+
+/** Dependency bindings for building a [UseCaseCamera] */
+@Module(includes = [UseCaseCamera.Bindings::class])
+abstract class UseCaseCameraModule {
+    // Used for dagger provider methods that are static.
+    companion object
+}
+
+/** Dagger module for binding the [UseCase]'s to the [UseCaseCamera]. */
+@Module
+class UseCaseCameraConfig(
+    private val useCases: List<UseCase>
+) {
+    @UseCaseCameraScope
+    @Provides
+    fun provideUseCaseList(): java.util.ArrayList<UseCase> {
+        return java.util.ArrayList(useCases)
+    }
+}
+
+/** Dagger subcomponent for a single [UseCaseCamera] instance. */
+@UseCaseCameraScope
+@Subcomponent(
+    modules = [
+        UseCaseCameraModule::class,
+        UseCaseCameraConfig::class
+    ]
+)
+interface UseCaseCameraComponent {
+    fun getUseCaseCamera(): UseCaseCamera
+
+    @Subcomponent.Builder
+    interface Builder {
+        fun config(config: UseCaseCameraConfig): Builder
+        fun build(): UseCaseCameraComponent
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
similarity index 78%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
index f9cb2fe..b7d2f6a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
@@ -14,7 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.camera2.pipe.integration.config;
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
deleted file mode 100644
index 3d24397..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.integration.impl
-
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraControlInternal
-import androidx.camera.core.impl.CameraInfoInternal
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.Quirks
-import androidx.camera.core.impl.utils.futures.Futures
-import com.google.common.util.concurrent.ListenableFuture
-
-/**
- * Adapt the [CameraInternal] class to one or more [CameraPipe] based Camera instances.
- */
-class CameraAdaptor(
-    private val cameraPipe: CameraPipe,
-    private val cameraId: CameraId
-) : CameraInternal {
-
-    init {
-        debug { "Created CameraAdaptor from $cameraPipe for $cameraId" }
-        // TODO: Consider preloading the list of camera ids and metadata.
-    }
-
-    // Load / unload methods
-    override fun open() {
-        TODO("Not yet implemented")
-    }
-
-    override fun close() {
-        TODO("Not yet implemented")
-    }
-
-    override fun release(): ListenableFuture<Void> {
-        // TODO: Determine what the correct way to invoke release is.
-        return Futures.immediateFuture(null)
-    }
-
-    // Static properties of this camera
-    override fun getCameraInfoInternal(): CameraInfoInternal {
-        TODO("Not yet implemented")
-    }
-
-    override fun getCameraQuirks(): Quirks {
-        TODO("Not yet implemented")
-    }
-
-    // Controls for interacting with or observing the state of the camera.
-    override fun getCameraState(): Observable<CameraInternal.State> {
-        TODO("Not yet implemented")
-    }
-
-    override fun getCameraControlInternal(): CameraControlInternal {
-        TODO("Not yet implemented")
-    }
-
-    // UseCase attach / detach behaviors.
-    override fun attachUseCases(useCases: MutableCollection<UseCase>) {
-        TODO("Not yet implemented")
-    }
-
-    override fun detachUseCases(useCases: MutableCollection<UseCase>) {
-        TODO("Not yet implemented")
-    }
-
-    // UseCase state callbacks
-    override fun onUseCaseActive(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-
-    override fun onUseCaseUpdated(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-
-    override fun onUseCaseReset(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-
-    override fun onUseCaseInactive(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
new file mode 100644
index 0000000..1619e7f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CaptureFailure
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.integration.adapter.CaptureResultAdapter
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureFailure
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * A map of [CameraCaptureCallback] that are invoked on each [Request].
+ */
+@CameraScope
+class CameraCallbackMap @Inject constructor() : Request.Listener {
+    private val callbackMap = mutableMapOf<CameraCaptureCallback, Executor>()
+    @Volatile
+    private var callbacks: Map<CameraCaptureCallback, Executor> = mapOf()
+
+    fun addCaptureCallback(callback: CameraCaptureCallback, executor: Executor) {
+        check(!callbacks.contains(callback)) { "$callback was already registered!" }
+
+        synchronized(callbackMap) {
+            callbackMap[callback] = executor
+            callbacks = callbackMap.toMap()
+        }
+    }
+
+    fun removeCaptureCallback(callback: CameraCaptureCallback) {
+        synchronized(callbackMap) {
+            callbackMap.remove(callback)
+            callbacks = callbackMap.toMap()
+        }
+    }
+
+    override fun onComplete(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        result: FrameInfo
+    ) {
+        val captureResult = CaptureResultAdapter(requestMetadata, frameNumber, result)
+        for ((callback, executor) in callbacks) {
+            executor.execute { callback.onCaptureCompleted(captureResult) }
+        }
+    }
+
+    override fun onFailed(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        captureFailure: CaptureFailure
+    ) {
+        val failure = CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR)
+        for ((callback, executor) in callbacks) {
+            executor.execute { callback.onCaptureFailed(failure) }
+        }
+    }
+
+    override fun onAborted(request: Request) {
+        for ((callback, executor) in callbacks) {
+            executor.execute { callback.onCaptureCancelled() }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
deleted file mode 100644
index 071da5d..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.camera.camera2.pipe.integration.impl
-
-import android.content.Context
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Debug
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Timestamps
-import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.CameraThreadConfig
-
-/**
- * The CameraPipeCameraFactory is responsible for creating and configuring CameraPipe for CameraX.
- */
-class CameraPipeFactory(
-    context: Context,
-    threadConfig: CameraThreadConfig
-) : CameraFactory {
-    // Lazily create and configure a CameraPipe instance.
-    private val cameraPipe: CameraPipe by lazy {
-        Debug.traceStart { "CameraPipeCameraFactory#cameraPipe" }
-        val result: CameraPipe?
-        val start = Timestamps.now()
-
-        // TODO: CameraPipe should find a way to make sure callbacks are executed on the configured
-        //   executors that are provided in `threadConfig`
-        debug { "TODO: Use $threadConfig if defined" }
-
-        result = CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
-        debug { "Created CameraPipe in ${start.measureNow().formatMs()}" }
-        Debug.traceStop()
-        result
-    }
-
-    init {
-        debug { "Created CameraPipeCameraFactory" }
-        // TODO: Consider preloading the list of camera ids and metadata.
-    }
-
-    override fun getCamera(cameraId: String): CameraInternal {
-        // TODO: The CameraInternal object is an facade that covers most of the high level camera
-        //   state and interactions. CameraInternal objects are persistent across camera switches.
-
-        return CameraAdaptor(cameraPipe, CameraId(cameraId))
-    }
-
-    override fun getAvailableCameraIds(): Set<String> {
-        // TODO: This may need some amount of work to limit the returned values well behaved "Front"
-        //   and "Back" camera devices.
-        return cameraPipe.cameras().findAll().map { it.value }.toSet()
-    }
-
-    override fun getCameraManager(): Any? {
-        // Note: This object is passed around as an untyped parameter when constructing a few
-        // objects (Such as `DeviceSurfaceManagerProvider`). It's better to rely on the parameter
-        // passing than to try to turn this object into a singleton.
-        return cameraPipe
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt
new file mode 100644
index 0000000..e66deba
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.annotation.SuppressLint
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.adapter.ExposureStateAdapter
+import androidx.camera.camera2.pipe.integration.adapter.ZoomStateAdapter
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.ExposureState
+import androidx.camera.core.ZoomState
+import androidx.lifecycle.MutableLiveData
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * [CameraState] caches and updates based on callbacks from the active CameraGraph.
+ */
+@SuppressLint("UnsafeExperimentalUsageError")
+@CameraScope
+class CameraState @Inject constructor(
+    private val cameraMetadata: Provider<CameraMetadata>,
+) {
+    val torchState = MutableLiveData<Int>()
+    val zoomState by lazy {
+        MutableLiveData<ZoomState>(
+            ZoomStateAdapter(
+                cameraMetadata.get(),
+                1.0f
+            )
+        )
+    }
+    val exposureState by lazy {
+        MutableLiveData<ExposureState>(
+            ExposureStateAdapter(
+                cameraMetadata.get(),
+                0
+            )
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt
new file mode 100644
index 0000000..aecf6ab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.graphics.Point
+import android.util.Size
+
+fun Size.area(): Int = this.width * this.height
+fun Size.asLandscape(): Size =
+    if (this.width >= this.height) this else Size(this.height, this.width)
+fun Size.asPortrait(): Size =
+    if (this.width <= this.height) this else Size(this.height, this.width)
+
+fun minByArea(left: Size, right: Size) = if (left.area() < right.area()) left else right
+fun maxByArea(left: Size, right: Size) = if (left.area() > right.area()) left else right
+
+fun Point.area(): Int = this.x * this.y
+fun Point.toSize() = Size(this.x, this.y)
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
deleted file mode 100644
index 56b5966..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.camera.camera2.pipe.integration.impl
-
-import android.content.Context
-import android.util.Size
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.core.impl.CameraDeviceSurfaceManager
-import androidx.camera.core.impl.SurfaceConfig
-import androidx.camera.core.impl.UseCaseConfig
-
-/**
- * Provide utilities for interacting with the set of guaranteed stream combinations.
- */
-class StreamConfigurationMap(context: Context, cameraManager: Any?) : CameraDeviceSurfaceManager {
-    private val cameraPipe: CameraPipe = cameraManager as CameraPipe
-
-    init {
-        debug { "Created StreamConfigurationMap from $context" }
-    }
-
-    override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
-        // TODO: This method needs to check to see if the list of SurfaceConfig's is in the map of
-        //   guaranteed stream configurations for this camera's support level.
-        return cameraPipe.cameras().findAll().contains(CameraId(cameraId))
-    }
-
-    override fun transformSurfaceConfig(
-        cameraId: String,
-        imageFormat: Int,
-        size: Size
-    ): SurfaceConfig? {
-        // TODO: Many of the "find a stream combination that will work" is already provided by the
-        //   existing camera2 implementation, and this implementation should leverage that work.
-
-        TODO("Not Implemented")
-    }
-
-    override fun getSuggestedResolutions(
-        cameraId: String,
-        existingSurfaces: List<SurfaceConfig>,
-        newUseCaseConfigs: List<UseCaseConfig<*>?>
-    ): Map<UseCaseConfig<*>, Size> {
-        // TODO: Many of the "find a stream combination that will work" is already provided by the
-        //   existing camera2 implementation, and this implementation should leverage that work.
-
-        TODO("Not Implemented")
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
similarity index 68%
rename from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
rename to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
index 8a6dfd3..75a1976 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
@@ -13,17 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.ui.test
 
-import org.jetbrains.skiko.Library
+package androidx.camera.camera2.pipe.integration.impl
 
-fun initCompose() {
-    ComposeInit
-}
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.core.impl.TagBundle
 
-private object ComposeInit {
-    init {
-        Library.load("/", "skiko")
-        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
-    }
-}
\ No newline at end of file
+/** Custom tags that can be passed used by CameraPipe */
+public val CAMERAX_TAG_BUNDLE = Metadata.Key.create<TagBundle>("camerax.tag_bundle")
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
new file mode 100644
index 0000000..088855a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CameraDevice
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestTemplate
+import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.StreamType
+import androidx.camera.camera2.pipe.TorchState
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.DeferrableSurface
+import dagger.Module
+import dagger.Provides
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+internal val useCaseCameraIds = atomic(0)
+
+/**
+ * API for interacting with a [CameraGraph] that has been configured with a set of [UseCase]'s
+ */
+class UseCaseCamera(
+    private val cameraGraph: CameraGraph,
+    private val useCases: List<UseCase>,
+    private val surfaceToStreamMap: Map<DeferrableSurface, StreamId>,
+    private val cameraScope: CoroutineScope
+) {
+    private val debugId = useCaseCameraIds.incrementAndGet()
+
+    private var _activeUseCases = setOf<UseCase>()
+
+    var activeUseCases: Set<UseCase>
+        get() = _activeUseCases
+        set(value) {
+            // Note: This may be called with the same set of values that was previously set. This
+            // is used as a signal to indicate the properties of the UseCase may have changed.
+            _activeUseCases = value
+            updateUseCases()
+        }
+
+    init {
+        debug { "Configured $this for $useCases" }
+    }
+
+    fun close() {
+        debug { "Closing $this" }
+        cameraGraph.close()
+    }
+
+    suspend fun enableTorchAsync(enabled: Boolean): Deferred<FrameNumber> {
+        return cameraGraph.acquireSession().use {
+            it.setTorch(
+                when (enabled) {
+                    true -> TorchState.ON
+                    false -> TorchState.OFF
+                }
+            )
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun updateUseCases() {
+        val repeatingStreamIds = mutableSetOf<StreamId>()
+        for (useCase in activeUseCases) {
+            val repeatingCapture = useCase.sessionConfig?.repeatingCaptureConfig
+            if (repeatingCapture != null) {
+                for (deferrableSurface in repeatingCapture.surfaces) {
+                    val streamId = surfaceToStreamMap[deferrableSurface]
+                    if (streamId != null) {
+                        repeatingStreamIds.add(streamId)
+                    }
+                }
+            }
+        }
+
+        // TODO: This needs to aggregate the current parameters and pass them to the request.
+
+        // In order to preserve ordering, this starts acquiring the session on the current thread,
+        // and will only switch to the cameraScope threads if it needs to suspend. This is important
+        // because access to the cameraGraph is well ordered, and if the coroutine suspends, it will
+        // resume in the order it accessed the cameraGraph.
+        cameraScope.launch(start = CoroutineStart.UNDISPATCHED) {
+            cameraGraph.acquireSession().use {
+                it.setRepeating(
+                    Request(
+                        streams = repeatingStreamIds.toList()
+                    )
+                )
+            }
+        }
+    }
+
+    override fun toString(): String = "UseCaseCamera-$debugId"
+
+    @Module
+    class Bindings {
+        companion object {
+            @UseCaseCameraScope
+            @Provides
+            fun provideCameraGraphController(
+                cameraPipe: CameraPipe,
+                useCases: java.util.ArrayList<UseCase>,
+                cameraConfig: CameraConfig,
+                callbackMap: CameraCallbackMap,
+                coroutineScope: CoroutineScope,
+            ): UseCaseCamera {
+                val streamConfigs = mutableListOf<StreamConfig>()
+                val useCaseMap = mutableMapOf<StreamConfig, UseCase>()
+
+                // TODO: This may need to combine outputs that are (or will) share the same output
+                //  imageReader or surface. Right now, each UseCase gets its own [StreamConfig]
+                // TODO: useCases only have a single `attachedSurfaceResolution`, yet they have a
+                //  list of deferrableSurfaces.
+                for (useCase in useCases) {
+                    val config = StreamConfig(
+                        size = useCase.attachedSurfaceResolution!!,
+                        format = StreamFormat(useCase.imageFormat),
+                        camera = cameraConfig.cameraId,
+                        type = StreamType.SURFACE,
+                        deferrable = false
+                    )
+                    streamConfigs.add(config)
+                    useCaseMap[config] = useCase
+                }
+
+                // Build up a config (using TEMPLATE_PREVIEW by default)
+                val config = CameraGraph.Config(
+                    camera = cameraConfig.cameraId,
+                    streams = streamConfigs,
+                    listeners = listOf(callbackMap),
+                    template = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
+                )
+                val graph = cameraPipe.create(config)
+
+                val surfaceToStreamMap = mutableMapOf<DeferrableSurface, StreamId>()
+                for ((streamConfig, useCase) in useCaseMap) {
+                    val stream = graph.streams[streamConfig]
+                    val useCaseSessionConfig = useCase.sessionConfig
+
+                    // TODO: UseCases have inconsistent opinions about how surfaces are handled,
+                    //  this code assumes only a single surface per UseCase.
+                    val deferredSurfaces = useCaseSessionConfig?.surfaces
+                    if (stream != null && deferredSurfaces != null && deferredSurfaces.size == 1) {
+                        val deferredSurface = deferredSurfaces[0]
+                        graph.setSurface(stream.id, deferredSurface.surface.get())
+                        surfaceToStreamMap[deferredSurface] = stream.id
+                    }
+                }
+
+                graph.start()
+                return UseCaseCamera(graph, useCases, surfaceToStreamMap, coroutineScope)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
deleted file mode 100644
index a0ddab2..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.integration.impl
-
-import android.content.Context
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.info
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.UseCaseConfigFactory
-
-/**
- * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
- *
- * This includes things like default template and session parameters, as well as maximum resolution
- * and aspect ratios for the display.
- */
-class UseCaseConfigurationMap(context: Context) : UseCaseConfigFactory {
-    init {
-        if (context === context.applicationContext) {
-            info {
-                "The provided context ($context) is application scoped and will be used to infer " +
-                    "the default display for computing the default preview size, orientation, " +
-                    "and default aspect ratio for UseCase outputs."
-            }
-        }
-        debug { "Created UseCaseConfigurationMap" }
-    }
-
-    /**
-     * Returns the configuration for the given capture type, or `null` if the configuration
-     * cannot be produced.
-     */
-    override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
-        TODO("Not Implemented")
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
new file mode 100644
index 0000000..85f4155
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
+import androidx.camera.core.UseCase
+import javax.inject.Inject
+
+/**
+ * This class keeps track of the currently attached and active [UseCase]'s for a specific camera.
+ */
+@CameraScope
+class UseCaseManager @Inject constructor(
+    private val cameraConfig: CameraConfig,
+    private val builder: UseCaseCameraComponent.Builder
+) {
+    private val attachedUseCases = mutableListOf<UseCase>()
+    private val enabledUseCases = mutableSetOf<UseCase>()
+
+    @Volatile
+    private var _activeComponent: UseCaseCameraComponent? = null
+    val camera: UseCaseCamera?
+        get() = _activeComponent?.getUseCaseCamera()
+
+    fun attach(useCases: List<UseCase>) {
+        if (useCases.isEmpty()) {
+            Log.warn { "Attach [] from $this (Ignored)" }
+            return
+        }
+        Log.debug { "Attaching $useCases from $this" }
+
+        var modified = false
+        for (useCase in useCases) {
+            if (!attachedUseCases.contains(useCase)) {
+                attachedUseCases.add(useCase)
+                modified = true
+            }
+        }
+
+        if (modified) {
+            start(attachedUseCases)
+        }
+    }
+
+    fun detach(useCases: List<UseCase>) {
+        if (useCases.isEmpty()) {
+            Log.warn { "Detaching [] from $this (Ignored)" }
+            return
+        }
+        Log.debug { "Detaching $useCases from $this" }
+
+        var modified = false
+        for (useCase in useCases) {
+            modified = attachedUseCases.remove(useCase) || modified
+        }
+
+        // TODO: We might only want to tear down when the number of attached use cases goes to
+        //  zero. If a single UseCase is removed, we could deactivate it?
+        if (modified) {
+            start(attachedUseCases)
+        }
+    }
+
+    fun enable(useCase: UseCase) {
+        if (enabledUseCases.add(useCase)) {
+            invalidate()
+        }
+    }
+
+    fun disable(useCase: UseCase) {
+        if (enabledUseCases.remove(useCase)) {
+            invalidate()
+        }
+    }
+
+    fun update(useCase: UseCase) {
+        if (attachedUseCases.contains(useCase)) {
+            invalidate()
+        }
+    }
+
+    override fun toString(): String = "UseCaseManager<${cameraConfig.cameraId}>"
+
+    private fun invalidate() {
+        camera?.let {
+            it.activeUseCases = enabledUseCases.toSet()
+        }
+    }
+
+    private fun start(newUseCases: List<UseCase>) {
+        val useCases = newUseCases.toList()
+
+        // Close prior camera graph
+        camera.let {
+            _activeComponent = null
+            it?.close()
+        }
+
+        // Update list of active useCases
+        if (useCases.isEmpty()) {
+            return
+        }
+
+        // Create and configure the new camera component.
+        _activeComponent = builder.config(UseCaseCameraConfig(useCases)).build()
+        invalidate()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/dependencies.gradle b/camera/camera-camera2-pipe/dependencies.gradle
index 6cd62b7..db00727 100644
--- a/camera/camera-camera2-pipe/dependencies.gradle
+++ b/camera/camera-camera2-pipe/dependencies.gradle
@@ -22,7 +22,7 @@
                 "androidx.annotation:annotation:1.0.0",
                 KOTLIN_STDLIB,
                 KOTLIN_COROUTINES_ANDROID,
-                "org.jetbrains.kotlinx:atomicfu:0.13.1"
+                "org.jetbrains.kotlinx:atomicfu:0.15.0"
         ],
         IMPLEMENTATION : [DAGGER]
     ]
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 1dddd47..c797ab9 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -95,7 +95,8 @@
     companion object Constants3A {
         // Constants related to controlling the time or frame budget a 3A operation should get.
         const val DEFAULT_FRAME_LIMIT = 60
-        const val DEFAULT_TIME_LIMIT_MS = 3000
+        const val DEFAULT_TIME_LIMIT_MS = 3_000
+        const val DEFAULT_TIME_LIMIT_NS = 3_000_000_000L
 
         // Constants related to metering regions.
         /** No metering region is specified. */
@@ -170,19 +171,19 @@
          *
          * @param frameLimit the maximum number of frames to wait before we give up waiting for
          * this operation to complete.
-         * @param timeLimitMs the maximum time limit in ms we wait before we give up waiting for
+         * @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
          * this operation to complete.
          *
          * @return [Result3A], which will contain the latest frame number at which the locks were
          * applied or the frame number at which the method returned early because either frame limit
          * or time limit was reached.
          */
-        fun lock3A(
+        suspend fun lock3A(
             aeLockBehavior: Lock3ABehavior? = null,
             afLockBehavior: Lock3ABehavior? = null,
             awbLockBehavior: Lock3ABehavior? = null,
             frameLimit: Int = DEFAULT_FRAME_LIMIT,
-            timeLimitMs: Int = DEFAULT_TIME_LIMIT_MS
+            timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
         ): Deferred<Result3A>
 
         /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index c7bb3b0..d514661 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -22,6 +22,9 @@
 import androidx.camera.camera2.pipe.impl.CameraPipeComponent
 import androidx.camera.camera2.pipe.impl.CameraPipeConfigModule
 import androidx.camera.camera2.pipe.impl.DaggerCameraPipeComponent
+import kotlinx.atomicfu.atomic
+
+internal val cameraPipeIds = atomic(0)
 
 /**
  * [CameraPipe] is the top level scope for all interactions with a Camera2 camera.
@@ -33,6 +36,7 @@
  * the [CameraGraph] interface.
  */
 class CameraPipe(config: Config) {
+    private val debugId = cameraPipeIds.incrementAndGet()
     private val component: CameraPipeComponent = DaggerCameraPipeComponent.builder()
         .cameraPipeConfigModule(CameraPipeConfigModule(config))
         .build()
@@ -63,4 +67,6 @@
         val appContext: Context,
         val cameraThread: HandlerThread? = null
     )
+
+    override fun toString(): String = "CameraPipe-$debugId"
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt
index 27406f7..5d594f7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt
@@ -41,4 +41,36 @@
      * Initiate a new scan, and then lock the values once the scan is done.
      */
     AFTER_NEW_SCAN,
-}
\ No newline at end of file
+}
+
+fun Lock3ABehavior?.shouldUnlockAe() =
+    this == Lock3ABehavior.AFTER_NEW_SCAN
+
+fun Lock3ABehavior?.shouldUnlockAf() =
+    this == Lock3ABehavior.AFTER_NEW_SCAN
+
+fun Lock3ABehavior?.shouldUnlockAwb() =
+    this == Lock3ABehavior.AFTER_NEW_SCAN
+
+// For ae and awb if we set the lock = true in the capture request the camera device
+// locks them immediately. So when we want to wait for ae to converge we have to explicitly
+// wait for it to converge.
+fun Lock3ABehavior?.shouldWaitForAeToConverge() =
+    this != null && this != Lock3ABehavior.IMMEDIATE
+
+fun Lock3ABehavior?.shouldWaitForAwbToConverge() =
+    this != null && this != Lock3ABehavior.IMMEDIATE
+
+// TODO(sushilnath@): add the optimization to not wait for af to converge before sending the
+// trigger for modes other than CONTINUOUS_VIDEO. The paragraph below explains the reasoning.
+//
+// For af, if the mode is MACRO, AUTO or CONTINUOUS_PICTURE and we send a capture request to
+// start an af trigger then camera device starts a new scan(for AUTO mode) or waits for the
+// current scan to finish(for CONTINUOUS_PICTURE) and then locks the auto-focus, so if we want
+// to wait for af to converge before locking it, we don't have to explicitly wait for
+// convergence, we can send the trigger right away, but if the mode is CONTINUOUS_VIDEO then
+// sending a request to start a trigger locks the auto focus immediately, so if we want af to
+// converge first then we have to explicitly wait for it.
+// Ref: https://developer.android.com/reference/android/hardware/camera2/CaptureResult#CONTROL_AF_STATE
+fun Lock3ABehavior?.shouldWaitForAfToConverge() =
+    this != null && this != Lock3ABehavior.IMMEDIATE
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index 0c3cdda..f961304 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -47,7 +47,7 @@
              * This will create a new Key instance, and will check to see that the key has not been
              * previously created somewhere else.
              */
-            internal fun <T> create(name: String): Key<T> {
+            fun <T> create(name: String): Key<T> {
                 synchronized(keys) {
                     check(keys.add(name)) { "$name is already defined!" }
                 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
index 9d5c485..2b7aa32 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
@@ -30,6 +30,7 @@
 import kotlinx.coroutines.Deferred
 
 internal val cameraGraphSessionIds = atomic(0)
+
 class CameraGraphSessionImpl(
     private val token: TokenLock.Token,
     private val graphProcessor: GraphProcessor,
@@ -84,14 +85,20 @@
         TODO("Implement setTorch")
     }
 
-    override fun lock3A(
+    override suspend fun lock3A(
         aeLockBehavior: Lock3ABehavior?,
         afLockBehavior: Lock3ABehavior?,
         awbLockBehavior: Lock3ABehavior?,
         frameLimit: Int,
-        timeLimitMs: Int
+        timeLimitNs: Long
     ): Deferred<Result3A> {
-        TODO("Implement lock3A")
+        // TODO(sushilnath): check if the device or the current mode supports lock for each of
+        // ae, af and awb respectively. If not supported return an exception or return early with
+        // the right status code.
+        return controller3A.lock3A(
+            aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit,
+            timeLimitNs
+        )
     }
 
     override fun lock3A(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
index df3b3c2..4de0475 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
@@ -16,16 +16,28 @@
 
 package androidx.camera.camera2.pipe.impl
 
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_START
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.params.MeteringRectangle
 import androidx.annotation.GuardedBy
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
+import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.shouldUnlockAe
+import androidx.camera.camera2.pipe.shouldUnlockAf
+import androidx.camera.camera2.pipe.shouldUnlockAwb
+import androidx.camera.camera2.pipe.shouldWaitForAeToConverge
+import androidx.camera.camera2.pipe.shouldWaitForAfToConverge
+import androidx.camera.camera2.pipe.shouldWaitForAwbToConverge
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.cancel
@@ -38,6 +50,66 @@
     private val graphState3A: GraphState3A,
     private val graphListener3A: Listener3A
 ) {
+    companion object {
+        private val aeConvergedStateList = listOf(
+            CaptureResult.CONTROL_AE_STATE_CONVERGED,
+            CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+            CaptureResult.CONTROL_AE_STATE_LOCKED
+        )
+
+        private val awbConvergedStateList = listOf(
+            CaptureResult.CONTROL_AWB_STATE_CONVERGED,
+            CaptureResult.CONTROL_AWB_STATE_LOCKED
+        )
+
+        private val afConvergedStateList = listOf(
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED,
+            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+        )
+
+        private val aeLockedStateList = listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
+
+        private val awbLockedStateList = listOf(CaptureResult.CONTROL_AWB_STATE_LOCKED)
+
+        private val afLockedStateList = listOf(
+            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+        )
+
+        val parameterForAfTriggerStart = mapOf<CaptureRequest.Key<*>, Any>(
+            CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START
+        )
+
+        val parameterForAfTriggerCancel = mapOf<CaptureRequest.Key<*>, Any>(
+            CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL
+        )
+
+        private val result3ASubmitFailed = Result3A(FRAME_NUMBER_INVALID, Status3A.SUBMIT_FAILED)
+
+        private val aeUnlockedStateList = listOf(
+            CaptureResult.CONTROL_AE_STATE_INACTIVE,
+            CaptureResult.CONTROL_AE_STATE_SEARCHING,
+            CaptureResult.CONTROL_AE_STATE_CONVERGED,
+            CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED
+        )
+
+        private val afUnlockedStateList = listOf(
+            CaptureResult.CONTROL_AF_STATE_INACTIVE,
+            CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED
+        )
+
+        private val awbUnlockedStateList = listOf(
+            CaptureResult.CONTROL_AWB_STATE_INACTIVE,
+            CaptureResult.CONTROL_AWB_STATE_SEARCHING,
+            CaptureResult.CONTROL_AWB_STATE_CONVERGED
+        )
+    }
+
     // Keep track of the result associated with latest call to update3A. If update3A is called again
     // and the current result is not complete, we will cancel the current result.
     @GuardedBy("this")
@@ -59,7 +131,7 @@
         // Update the 3A state of the graph. This will make sure then when GraphProcessor builds
         // the next request it will apply the 3A parameters corresponding to the updated 3A state
         // to the request.
-        graphState3A.update(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
+        graphState3A.update(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions, null, null)
         // Try submitting a new repeating request with the 3A parameters corresponding to the new
         // 3A state and corresponding listeners.
         graphProcessor.invalidate()
@@ -110,13 +182,261 @@
 
         if (!graphProcessor.submit(extra3AParams)) {
             graphListener3A.removeListener(listener)
-            return CompletableDeferred(
-                Result3A(FRAME_NUMBER_INVALID, Status3A.SUBMIT_FAILED)
-            )
+            return CompletableDeferred(result3ASubmitFailed)
         }
         return listener.getDeferredResult()
     }
 
+    /**
+     * Given the desired lock behaviors for ae, af and awb, this method, (a) first unlocks them and
+     * wait for them to converge, and then (b) locks them.
+     *
+     * (a) In this step, as needed, we first send a single request with 'af trigger = cancel' to
+     * unlock af, and then a repeating request to unlock ae and awb. We suspend till we receive a
+     * response from the camera that each of the ae, af awb are converged.
+     * (b) In this step, as needed, we submit a repeating request to lock ae and awb, and then a
+     * single request to lock af by setting 'af trigger = start'. Once these requests are submitted
+     * we don't wait further and immediately return a Deferred<Result3A> which gets completed when
+     * the capture result with correct lock states for ae, af and awb is received.
+     *
+     * If we received an error when submitting any of the above requests or if waiting for the
+     * desired 3A state times out then we return early with the appropriate status code.
+     *
+     * Note: the frameLimit and timeLimitNs applies to each of the above steps (a) and (b) and not
+     * as a whole for the whole lock3A method. Thus, in the worst case this method including the
+     * completion of returned Deferred<Result3A> can take 2 * min(time equivalent of frameLimit,
+     * timeLimit) to complete
+     */
+    suspend fun lock3A(
+        aeLockBehavior: Lock3ABehavior? = null,
+        afLockBehavior: Lock3ABehavior? = null,
+        awbLockBehavior: Lock3ABehavior? = null,
+        frameLimit: Int = CameraGraph.DEFAULT_FRAME_LIMIT,
+        timeLimitMsNs: Long? = CameraGraph.DEFAULT_TIME_LIMIT_NS
+    ): Deferred<Result3A> {
+        // If we explicitly need to unlock af first before proceeding to lock it, we need to send
+        // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
+        if (afLockBehavior.shouldUnlockAf()) {
+            debug { "lock3A - sending a request to unlock af first." }
+            if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+                return CompletableDeferred(result3ASubmitFailed)
+            }
+        }
+
+        // As needed unlock ae, awb and wait for ae, af and awb to converge.
+        if (aeLockBehavior.shouldWaitForAeToConverge() ||
+            afLockBehavior.shouldWaitForAfToConverge() ||
+            awbLockBehavior.shouldWaitForAwbToConverge()
+        ) {
+            val converged3AExitConditions = createConverged3AExitConditions(
+                aeLockBehavior.shouldWaitForAeToConverge(),
+                afLockBehavior.shouldWaitForAfToConverge(),
+                awbLockBehavior.shouldWaitForAwbToConverge()
+            )
+            val listener = Result3AStateListenerImpl(
+                converged3AExitConditions,
+                frameLimit,
+                timeLimitMsNs
+            )
+            graphListener3A.addListener(listener)
+
+            // If we have to explicitly unlock ae, awb, then update the 3A state of the camera
+            // graph. This is because ae, awb lock values should stay as part of repeating
+            // request to the camera device. For af we need only one single request to trigger it,
+            // leaving it unset in the subsequent requests to the camera device will not affect the
+            // previously sent af trigger.
+            val aeLockValue = if (aeLockBehavior.shouldUnlockAe()) false else null
+            val awbLockValue = if (awbLockBehavior.shouldUnlockAwb()) false else null
+            if (aeLockValue != null || awbLockValue != null) {
+                debug { "lock3A - setting aeLock=$aeLockValue, awbLock=$awbLockValue" }
+                graphState3A.update(
+                    aeLock = aeLockValue,
+                    awbLock = awbLockValue
+                )
+            }
+            graphProcessor.invalidate()
+
+            debug {
+                "lock3A - waiting for" +
+                    (if (aeLockBehavior.shouldWaitForAeToConverge()) " ae" else "") +
+                    (if (afLockBehavior.shouldWaitForAfToConverge()) " af" else "") +
+                    (if (awbLockBehavior.shouldWaitForAwbToConverge()) " awb" else "") +
+                    " to converge before locking them."
+            }
+            val result = listener.getDeferredResult().await()
+            debug {
+                "lock3A - converged at frame number=${result.frameNumber.value}, status=${result
+                    .status}"
+            }
+            // Return immediately if we encounter an error when unlocking and waiting for
+            // convergence.
+            if (result.status != Status3A.OK) {
+                return CompletableDeferred(result)
+            }
+        }
+
+        return lock3ANow(aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit, timeLimitMsNs)
+    }
+
+    /**
+     * This method unlocks ae, af and awb, as specified by setting the corresponding parameter to
+     * true.
+     *
+     * There are two requests involved in this operation, (a) a single request with af trigger =
+     * cancel, to unlock af, and then (a) a repeating request to unlock ae, awb.
+     */
+    suspend fun unlock3A(
+        ae: Boolean? = null,
+        af: Boolean? = null,
+        awb: Boolean? = null
+    ): Deferred<Result3A> {
+        check(ae == true || af == true || awb == true) { "No parameter has value as true" }
+        // If we explicitly need to unlock af first before proceeding to lock it, we need to send
+        // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
+        if (af == true) {
+            debug { "unlock3A - sending a request to unlock af first." }
+            if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+                debug { "unlock3A - request to unlock af failed, returning early." }
+                return CompletableDeferred(result3ASubmitFailed)
+            }
+        }
+
+        // As needed unlock ae, awb and wait for ae, af and awb to converge.
+        val unlocked3AExitConditions = createUnLocked3AExitConditions(
+            ae == true,
+            af == true,
+            awb == true
+        )
+        val listener = Result3AStateListenerImpl(unlocked3AExitConditions)
+        graphListener3A.addListener(listener)
+
+        // Update the 3A state of the camera graph and invalidate the repeating request with the
+        // new state.
+        val aeLockValue = if (ae == true) false else null
+        val awbLockValue = if (awb == true) false else null
+        if (aeLockValue != null || awbLockValue != null) {
+            debug { "unlock3A - updating graph state, aeLock=$aeLockValue, awbLock=$awbLockValue" }
+            graphState3A.update(
+                aeLock = aeLockValue,
+                awbLock = awbLockValue
+            )
+        }
+        graphProcessor.invalidate()
+        return listener.getDeferredResult()
+    }
+
+    private suspend fun lock3ANow(
+        aeLockBehavior: Lock3ABehavior?,
+        afLockBehavior: Lock3ABehavior?,
+        awbLockBehavior: Lock3ABehavior?,
+        frameLimit: Int?,
+        timeLimitMsNs: Long?
+    ): Deferred<Result3A> {
+        val finalAeLockValue = if (aeLockBehavior == null) null else true
+        val finalAwbLockValue = if (awbLockBehavior == null) null else true
+        val locked3AExitConditions = createLocked3AExitConditions(
+            finalAeLockValue != null,
+            afLockBehavior != null,
+            finalAwbLockValue != null
+        )
+
+        var resultForLocked: Deferred<Result3A>? = null
+        if (locked3AExitConditions.isNotEmpty()) {
+            val listener = Result3AStateListenerImpl(
+                locked3AExitConditions,
+                frameLimit,
+                timeLimitMsNs
+            )
+            graphListener3A.addListener(listener)
+            graphState3A.update(aeLock = finalAeLockValue, awbLock = finalAwbLockValue)
+            debug {
+                "lock3A - submitting request with aeLock=$finalAeLockValue , " +
+                    "awbLock=$finalAwbLockValue"
+            }
+            graphProcessor.invalidate()
+            resultForLocked = listener.getDeferredResult()
+        }
+
+        if (afLockBehavior == null) {
+            return resultForLocked!!
+        }
+
+        debug { "lock3A - submitting a request to lock af." }
+        if (!graphProcessor.submit(parameterForAfTriggerStart)) {
+            // TODO(sushilnath@): Change the error code to a more specific code so it's clear
+            // that one of the request in sequence of requests failed and the caller should
+            // unlock 3A to bring the 3A system to an initial state and then try again if they
+            // want to. The other option is to reset or restore the 3A state here.
+            return CompletableDeferred(result3ASubmitFailed)
+        }
+        return resultForLocked!!
+    }
+
+    private fun createConverged3AExitConditions(
+        waitForAeToConverge: Boolean,
+        waitForAfToConverge: Boolean,
+        waitForAwbToConverge: Boolean
+    ): Map<CaptureResult.Key<*>, List<Any>> {
+        if (
+            !waitForAeToConverge && !waitForAfToConverge && !waitForAwbToConverge
+        ) {
+            return mapOf()
+        }
+        val exitConditionMapForConverged = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
+        if (waitForAeToConverge) {
+            exitConditionMapForConverged[CaptureResult.CONTROL_AE_STATE] = aeConvergedStateList
+        }
+        if (waitForAwbToConverge) {
+            exitConditionMapForConverged[CaptureResult.CONTROL_AWB_STATE] = awbConvergedStateList
+        }
+        if (waitForAfToConverge) {
+            exitConditionMapForConverged[CaptureResult.CONTROL_AF_STATE] = afConvergedStateList
+        }
+        return exitConditionMapForConverged
+    }
+
+    private fun createLocked3AExitConditions(
+        waitForAeToLock: Boolean,
+        waitForAfToLock: Boolean,
+        waitForAwbToLock: Boolean
+    ): Map<CaptureResult.Key<*>, List<Any>> {
+        if (!waitForAeToLock && !waitForAfToLock && !waitForAwbToLock) {
+            return mapOf()
+        }
+        val exitConditionMapForLocked = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
+        if (waitForAeToLock) {
+            exitConditionMapForLocked[CaptureResult.CONTROL_AE_STATE] = aeLockedStateList
+        }
+        if (waitForAfToLock) {
+            exitConditionMapForLocked[CaptureResult.CONTROL_AF_STATE] = afLockedStateList
+        }
+        if (waitForAwbToLock) {
+            exitConditionMapForLocked[CaptureResult.CONTROL_AWB_STATE] = awbLockedStateList
+        }
+        return exitConditionMapForLocked
+    }
+
+    private fun createUnLocked3AExitConditions(
+        ae: Boolean,
+        af: Boolean,
+        awb: Boolean
+    ): Map<CaptureResult.Key<*>, List<Any>> {
+        if (!ae && !af && !awb) {
+            return mapOf()
+        }
+        val exitConditionMapForUnLocked = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
+        if (ae) {
+            exitConditionMapForUnLocked[CaptureResult.CONTROL_AE_STATE] = aeUnlockedStateList
+        }
+        if (af) {
+            exitConditionMapForUnLocked[CaptureResult.CONTROL_AF_STATE] = afUnlockedStateList
+        }
+        if (awb) {
+            exitConditionMapForUnLocked[CaptureResult.CONTROL_AWB_STATE] = awbUnlockedStateList
+        }
+        return exitConditionMapForUnLocked
+    }
+
     // We create a map for the 3A modes and the desired values and leave out the keys
     // corresponding to the metering regions. The reason being the camera framework can chose to
     // crop or modify the metering regions as per its constraints. So when we receive at least
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
index b69476c..97664c0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
@@ -28,6 +28,12 @@
  *
  * This object is used to maintain the key-value pairs for the most recent 3A state that is used
  * when building the requests that are sent to a CameraCaptureSession.
+ *
+ * The state is comprised of the modes, metering regions for ae, af and awb, and locks for ae and
+ * awb. We don't track the lock for af since af lock is achieved by setting 'af trigger = start' in
+ * in a request and then omitting the af trigger field in the subsequent requests doesn't disturb
+ * the af state. However for ae and awb, the lock type is boolean and should be explicitly set to
+ * 'true' in the subsequent requests once we have locked ae/awb and want them to stay locked.
  */
 @CameraGraphScope
 class GraphState3A @Inject constructor() {
@@ -37,14 +43,18 @@
     private var aeRegions: List<MeteringRectangle>? = null
     private var afRegions: List<MeteringRectangle>? = null
     private var awbRegions: List<MeteringRectangle>? = null
+    private var aeLock: Boolean? = null
+    private var awbLock: Boolean? = null
 
     fun update(
-        aeMode: AeMode?,
-        afMode: AfMode?,
-        awbMode: AwbMode?,
-        aeRegions: List<MeteringRectangle>?,
-        afRegions: List<MeteringRectangle>?,
-        awbRegions: List<MeteringRectangle>?
+        aeMode: AeMode? = null,
+        afMode: AfMode? = null,
+        awbMode: AwbMode? = null,
+        aeRegions: List<MeteringRectangle>? = null,
+        afRegions: List<MeteringRectangle>? = null,
+        awbRegions: List<MeteringRectangle>? = null,
+        aeLock: Boolean? = null,
+        awbLock: Boolean? = null
     ) {
         synchronized(this) {
             aeMode?.let { this.aeMode = it }
@@ -53,6 +63,8 @@
             aeRegions?.let { this.aeRegions = it }
             afRegions?.let { this.afRegions = it }
             awbRegions?.let { this.awbRegions = it }
+            aeLock?.let { this.aeLock = it }
+            awbLock?.let { this.awbLock = it }
         }
     }
 
@@ -65,6 +77,8 @@
             aeRegions?.let { map.put(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
             afRegions?.let { map.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
             awbRegions?.let { map.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
+            aeLock?.let { map.put(CaptureRequest.CONTROL_AE_LOCK, it) }
+            awbLock?.let { map.put(CaptureRequest.CONTROL_AWB_LOCK, it) }
             return map
         }
     }
@@ -77,6 +91,8 @@
             aeRegions?.let { builder.set(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
             afRegions?.let { builder.set(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
             awbRegions?.let { builder.set(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
+            aeLock?.let { builder.set(CaptureRequest.CONTROL_AE_LOCK, it) }
+            awbLock?.let { builder.set(CaptureRequest.CONTROL_AWB_LOCK, it) }
         }
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
index 5204cc8..3a87a61 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
@@ -277,7 +277,7 @@
 
                 val surface = surfaceMap[stream]
                 if (surface != null) {
-                    Log.debug { "  Binding $surface to $stream" }
+                    Log.debug { "  Binding $stream to $surface" }
 
                     // TODO(codelogic) There should be a more efficient way to do these lookups than
                     // having two maps.
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
new file mode 100644
index 0000000..8d837e7
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
@@ -0,0 +1,641 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Lock3ABehavior
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class Controller3ALock3ATest {
+    private val graphProcessor = FakeGraphProcessor()
+    private val graphState3A = GraphState3A()
+    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val listener3A = Listener3A()
+    private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
+
+    @Test
+    fun testAfImmediateAeImmediate(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val result = controller3A.lock3A(
+            afLockBehavior = Lock3ABehavior.IMMEDIATE,
+            aeLockBehavior = Lock3ABehavior.IMMEDIATE
+        )
+        assertThat(result.isCompleted).isFalse()
+
+        // Since requirement of to lock both AE and AF immediately, the requests to lock AE and AF
+        // are sent right away. The result of lock3A call will complete once AE and AF have reached
+        // their desired states. In this response i.e cameraResponse1, AF is still scanning so the
+        // result won't be complete.
+        val cameraResponse = GlobalScope.async {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        cameraResponse.await()
+        assertThat(result.isCompleted).isFalse()
+
+        // One we we are notified that the AE and AF are in locked state, the result of lock3A call
+        // will complete.
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+        // We not check if the correct sequence of requests were submitted by lock3A call. The
+        // request should be a repeating request to lock AE.
+        val request1 = requestProcessor.nextEvent().request
+        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        // The second request should be a single request to lock AF.
+        val request2 = requestProcessor.nextEvent().request
+        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+        assertThat(request2.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+    }
+
+    @Test
+    fun testAfImmediateAeAfterCurrentScan(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val lock3AAsyncTask = GlobalScope.async {
+            controller3A.lock3A(
+                afLockBehavior = Lock3ABehavior.IMMEDIATE,
+                aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
+            )
+        }
+        assertThat(lock3AAsyncTask.isCompleted).isFalse()
+        // Launch a task to repeatedly invoke a given capture result.
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED
+                        )
+                    )
+                )
+                delay(Companion.FRAME_RATE_MS)
+            }
+        }
+
+        val result = lock3AAsyncTask.await()
+        // Result of lock3A call shouldn't be complete yet since the AE and AF are not locked yet.
+        assertThat(result.isCompleted).isFalse()
+
+        // Check the correctness of the requests submitted by lock3A.
+        // One repeating request was sent to monitor the state of AE to get converged.
+        requestProcessor.nextEvent().request
+        // Once AE is converged, another repeatingrequest is sent to lock AE.
+        val request1 = requestProcessor.nextEvent().request
+        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+        // A single request to lock AF must have been used as well.
+        val request2 = requestProcessor.nextEvent().request
+        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+    }
+
+    @Test
+    fun testAfImmediateAeAfterNewScan(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val lock3AAsyncTask = GlobalScope.async {
+            controller3A.lock3A(
+                afLockBehavior = Lock3ABehavior.IMMEDIATE,
+                aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN
+            )
+        }
+        assertThat(lock3AAsyncTask.isCompleted).isFalse()
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = lock3AAsyncTask.await()
+        assertThat(result.isCompleted).isFalse()
+
+        // For a new AE scan we first send a request to unlock AE just in case it was
+        // previously or internally locked.
+        val request1 = requestProcessor.nextEvent().request
+        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            false
+        )
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+        // There should be one more request to lock AE after new scan is done.
+        val request2 = requestProcessor.nextEvent().request
+        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        // And one request to lock AF.
+        val request3 = requestProcessor.nextEvent().request
+        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+    }
+
+    @Test
+    fun testAfAfterCurrentScanAeImmediate(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val lock3AAsyncTask = GlobalScope.async {
+            controller3A.lock3A(
+                afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+                aeLockBehavior = Lock3ABehavior.IMMEDIATE
+            )
+        }
+        assertThat(lock3AAsyncTask.isCompleted).isFalse()
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = lock3AAsyncTask.await()
+        assertThat(result.isCompleted).isFalse()
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+        // There should be one request to monitor AF to finish it's scan.
+        requestProcessor.nextEvent()
+        // One request to lock AE
+        val request2 = requestProcessor.nextEvent().request
+        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        // And one request to lock AF.
+        val request3 = requestProcessor.nextEvent().request
+        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+    }
+
+    @Test
+    fun testAfAfterNewScanScanAeImmediate(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val lock3AAsyncTask = GlobalScope.async {
+            controller3A.lock3A(
+                afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
+                aeLockBehavior = Lock3ABehavior.IMMEDIATE
+            )
+        }
+        assertThat(lock3AAsyncTask.isCompleted).isFalse()
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = lock3AAsyncTask.await()
+        assertThat(result.isCompleted).isFalse()
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+        // One request to cancel AF to start a new scan.
+        val request1 = requestProcessor.nextEvent().request
+        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+        )
+        // There should be one request to monitor AF to finish it's scan.
+        requestProcessor.nextEvent()
+
+        // There should be one request to monitor lock AE.
+        val request2 = requestProcessor.nextEvent().request
+        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        // And one request to lock AF.
+        val request3 = requestProcessor.nextEvent().request
+        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+    }
+
+    @Test
+    fun testAfAfterCurrentScanAeAfterCurrentScan(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val lock3AAsyncTask = GlobalScope.async {
+            controller3A.lock3A(
+                afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+                aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
+            )
+        }
+        assertThat(lock3AAsyncTask.isCompleted).isFalse()
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = lock3AAsyncTask.await()
+        assertThat(result.isCompleted).isFalse()
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+        // There should be one request to monitor AF to finish it's scan.
+        requestProcessor.nextEvent()
+        // One request to lock AE
+        val request2 = requestProcessor.nextEvent().request
+        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        // And one request to lock AF.
+        val request3 = requestProcessor.nextEvent().request
+        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+        assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+    }
+
+    @Test
+    fun testAfAfterNewScanScanAeAfterNewScan(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val lock3AAsyncTask = GlobalScope.async {
+            controller3A.lock3A(
+                afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
+                aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN
+            )
+        }
+        assertThat(lock3AAsyncTask.isCompleted).isFalse()
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_CONVERGED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = lock3AAsyncTask.await()
+        assertThat(result.isCompleted).isFalse()
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+        // One request to cancel AF to start a new scan.
+        val request1 = requestProcessor.nextEvent().request
+        assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+        )
+        // There should be one request to unlock AE and monitor the current AF scan to finish.
+        val request2 = requestProcessor.nextEvent().request
+        assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            false
+        )
+
+        // There should be one request to monitor lock AE.
+        val request3 = requestProcessor.nextEvent().request
+        assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        // And one request to lock AF.
+        val request4 = requestProcessor.nextEvent().request
+        assertThat(request4!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+        assertThat(request4.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+    }
+
+    private fun initGraphProcessor() {
+        graphProcessor.attach(requestProcessor)
+        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+    }
+
+    companion object {
+        // The time duration in milliseconds between two frame results.
+        private const val FRAME_RATE_MS = 33L
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
index bf5ea3f..c87f5c0 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
@@ -45,9 +45,9 @@
 @RunWith(CameraPipeRobolectricTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class Controller3ASubmit3ATest {
-    private val requestProcessor = FakeRequestProcessor()
     private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
+    private val requestProcessor = FakeRequestProcessor(graphState3A)
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
new file mode 100644
index 0000000..ad3d24c
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import com.google.common.truth.Truth
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class Controller3AUnlock3ATest {
+    private val graphProcessor = FakeGraphProcessor()
+    private val graphState3A = GraphState3A()
+    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val listener3A = Listener3A()
+    private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
+
+    @Test
+    fun testUnlockAe(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val unLock3AAsyncTask = GlobalScope.async {
+            controller3A.unlock3A(ae = true)
+        }
+
+        // Launch a task to repeatedly invoke a given capture result.
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AE_STATE to
+                                CaptureResult.CONTROL_AE_STATE_LOCKED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = unLock3AAsyncTask.await()
+        // Result of unlock3A call shouldn't be complete yet since the AE is locked.
+        Truth.assertThat(result.isCompleted).isFalse()
+
+        // There should be one request to lock AE.
+        val request1 = requestProcessor.nextEvent().request
+        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+            .isEqualTo(false)
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+    }
+
+    @Test
+    fun testUnlockAf(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val unLock3AAsyncTask = GlobalScope.async { controller3A.unlock3A(af = true) }
+
+        // Launch a task to repeatedly invoke a given capture result.
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = unLock3AAsyncTask.await()
+        // Result of unlock3A call shouldn't be complete yet since the AF is locked.
+        Truth.assertThat(result.isCompleted).isFalse()
+
+        // There should be one request to unlock AF.
+        val request1 = requestProcessor.nextEvent().request
+        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+    }
+
+    @Test
+    fun testUnlockAwb(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val unLock3AAsyncTask = GlobalScope.async {
+            controller3A.unlock3A(awb = true)
+        }
+
+        // Launch a task to repeatedly invoke a given capture result.
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AWB_STATE to
+                                CaptureResult.CONTROL_AWB_STATE_LOCKED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = unLock3AAsyncTask.await()
+        // Result of unlock3A call shouldn't be complete yet since the AWB is locked.
+        Truth.assertThat(result.isCompleted).isFalse()
+
+        // There should be one request to lock AWB.
+        val request1 = requestProcessor.nextEvent().request
+        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AWB_LOCK])
+            .isEqualTo(false)
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_SEARCHING
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+    }
+
+    @Test
+    fun testUnlockAeAf(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val unLock3AAsyncTask = GlobalScope.async { controller3A.unlock3A(ae = true, af = true) }
+
+        // Launch a task to repeatedly invoke a given capture result.
+        GlobalScope.launch {
+            while (true) {
+                listener3A.onRequestSequenceCreated(
+                    FakeRequestMetadata(
+                        requestNumber = RequestNumber(1)
+                    )
+                )
+                listener3A.onPartialCaptureResult(
+                    FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                    FrameNumber(101L),
+                    FakeFrameMetadata(
+                        frameNumber = FrameNumber(101L),
+                        resultMetadata = mapOf(
+                            CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED,
+                            CaptureResult.CONTROL_AF_STATE to
+                                CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+                        )
+                    )
+                )
+                delay(FRAME_RATE_MS)
+            }
+        }
+
+        val result = unLock3AAsyncTask.await()
+        // Result of unlock3A call shouldn't be complete yet since the AF is locked.
+        Truth.assertThat(result.isCompleted).isFalse()
+
+        // There should be one request to unlock AF.
+        val request1 = requestProcessor.nextEvent().request
+        Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+            .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+        // Then request to unlock AE.
+        val request2 = requestProcessor.nextEvent().request
+        Truth.assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+            .isEqualTo(false)
+
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+    }
+
+    private fun initGraphProcessor() {
+        graphProcessor.attach(requestProcessor)
+        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+    }
+
+    companion object {
+        // The time duration in milliseconds between two frame results.
+        private const val FRAME_RATE_MS = 33L
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
index 833e885..48b00fa 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
@@ -24,12 +24,15 @@
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
 import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -45,11 +48,14 @@
 class Controller3AUpdate3ATest {
     private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
+    private val requestProcessor = FakeRequestProcessor(graphState3A)
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
     @Test
     fun testUpdate3AUpdatesState3A() {
+        initGraphProcessor()
+
         val result = controller3A.update3A(afMode = AfMode.OFF)
         assertThat(graphState3A.readState()[CaptureRequest.CONTROL_AF_MODE]).isEqualTo(
             CaptureRequest.CONTROL_AE_MODE_OFF
@@ -60,6 +66,8 @@
     @ExperimentalCoroutinesApi
     @Test
     fun testUpdate3ACancelsPreviousInProgressUpdate() {
+        initGraphProcessor()
+
         val result = controller3A.update3A(afMode = AfMode.OFF)
         // Invoking update3A before the previous one is complete will cancel the result of the
         // previous call.
@@ -69,6 +77,8 @@
 
     @Test
     fun testAfModeUpdate(): Unit = runBlocking {
+        initGraphProcessor()
+
         val result = controller3A.update3A(afMode = AfMode.OFF)
         GlobalScope.launch {
             listener3A.onRequestSequenceCreated(
@@ -94,6 +104,8 @@
 
     @Test
     fun testAeModeUpdate(): Unit = runBlocking {
+        initGraphProcessor()
+
         val result = controller3A.update3A(aeMode = AeMode.ON_ALWAYS_FLASH)
         GlobalScope.launch {
             listener3A.onRequestSequenceCreated(
@@ -120,6 +132,8 @@
 
     @Test
     fun testAwbModeUpdate(): Unit = runBlocking {
+        initGraphProcessor()
+
         val result = controller3A.update3A(awbMode = AwbMode.CLOUDY_DAYLIGHT)
         GlobalScope.launch {
             listener3A.onRequestSequenceCreated(
@@ -146,6 +160,8 @@
 
     @Test
     fun testAfRegionsUpdate(): Unit = runBlocking {
+        initGraphProcessor()
+
         val result = controller3A.update3A(afRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
         GlobalScope.launch {
             listener3A.onRequestSequenceCreated(
@@ -172,6 +188,8 @@
 
     @Test
     fun testAeRegionsUpdate(): Unit = runBlocking {
+        initGraphProcessor()
+
         val result = controller3A.update3A(aeRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
         GlobalScope.launch {
             listener3A.onRequestSequenceCreated(
@@ -198,6 +216,8 @@
 
     @Test
     fun testAwbRegionsUpdate(): Unit = runBlocking {
+        initGraphProcessor()
+
         val result = controller3A.update3A(
             awbRegions = listOf(
                 MeteringRectangle(1, 1, 100, 100, 2)
@@ -225,4 +245,9 @@
         assertThat(result3A.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Status3A.OK)
     }
+
+    private fun initGraphProcessor() {
+        graphProcessor.attach(requestProcessor)
+        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
index abdc307..23c63d1 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
@@ -36,8 +36,8 @@
 class GraphProcessorTest {
     private val globalListener = FakeRequestListener()
 
-    private val fakeProcessor1 = FakeRequestProcessor()
-    private val fakeProcessor2 = FakeRequestProcessor()
+    private val fakeProcessor1 = FakeRequestProcessor(GraphState3A())
+    private val fakeProcessor2 = FakeRequestProcessor(GraphState3A())
 
     private val requestListener1 = FakeRequestListener()
     private val request1 = Request(listOf(StreamId(0)), listeners = listOf(requestListener1))
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
index 730a1dd..f8007e9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
@@ -105,7 +105,7 @@
             virtualSessionState = VirtualSessionState(
                 FakeGraphProcessor(),
                 sessionFactory,
-                FakeRequestProcessor(),
+                FakeRequestProcessor(GraphState3A()),
                 this
             )
         )
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 139f0b0..5ab64bc 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -93,5 +93,6 @@
     }
 
     override fun invalidate() {
+        processor!!.setRepeating(repeatingRequest!!, mapOf(), false)
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
index f7e3bed..0f446d5 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
@@ -20,6 +20,7 @@
 import android.view.Surface
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.impl.GraphState3A
 import androidx.camera.camera2.pipe.impl.RequestProcessor
 import androidx.camera.camera2.pipe.impl.TokenLock
 import androidx.camera.camera2.pipe.impl.TokenLockImpl
@@ -30,7 +31,8 @@
 /**
  * Fake implementation of a [RequestProcessor] for tests.
  */
-class FakeRequestProcessor : RequestProcessor, RequestProcessor.Factory {
+class FakeRequestProcessor(private val graphState3A: GraphState3A) :
+    RequestProcessor, RequestProcessor.Factory {
     private val eventChannel = Channel<Event>(Channel.UNLIMITED)
 
     val requestQueue: MutableList<FakeRequest> = mutableListOf()
@@ -71,7 +73,7 @@
         requireSurfacesForAllStreams: Boolean
     ): Boolean {
         val fakeRequest =
-            FakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+            createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
 
         if (rejectRequests || closeInvoked) {
             check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
@@ -90,7 +92,7 @@
         requireSurfacesForAllStreams: Boolean
     ): Boolean {
         val fakeRequest =
-            FakeRequest(requests, extraRequestParameters, requireSurfacesForAllStreams)
+            createFakeRequest(requests, extraRequestParameters, requireSurfacesForAllStreams)
         if (rejectRequests || closeInvoked) {
             check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
             return false
@@ -108,7 +110,7 @@
         requireSurfacesForAllStreams: Boolean
     ): Boolean {
         val fakeRequest =
-            FakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+            createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
         if (rejectRequests || closeInvoked) {
             check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
             return false
@@ -137,9 +139,20 @@
     /**
      * Get the next event from queue with an option to specify a timeout for tests.
      */
-    suspend fun nextEvent(timeMillis: Long = 100): Event = withTimeout(timeMillis) {
+    suspend fun nextEvent(timeMillis: Long = 500): Event = withTimeout(timeMillis) {
         eventChannel.receive()
     }
+
+    private fun createFakeRequest(
+        burst: List<Request>,
+        extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
+        requireStreams: Boolean
+    ): FakeRequest {
+        val parameterMap = mutableMapOf<CaptureRequest.Key<*>, Any>()
+        parameterMap.putAll(graphState3A.readState())
+        parameterMap.putAll(extraRequestParameters)
+        return FakeRequest(burst, parameterMap, requireStreams)
+    }
 }
 
 data class Event(
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 33822d5..02e92f9 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -63,7 +63,8 @@
     androidTestImplementation(KOTLIN_COROUTINES_ANDROID)
     androidTestImplementation(project(":annotation:annotation-experimental"))
     androidTestImplementation(project(":internal-testutils-truth"))
-    androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.13.1")
+    androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.15.0")
+    androidTestImplementation("androidx.exifinterface:exifinterface:1.0.0")
 }
 android {
     defaultConfig {
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java
index ef5af22..5001b38 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java
@@ -16,10 +16,14 @@
 
 package androidx.camera.camera2.internal;
 
+import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_FIRED;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.when;
 
+import android.graphics.Rect;
+import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.CaptureResult;
 
 import androidx.camera.core.impl.CameraCaptureMetaData.AeState;
@@ -28,6 +32,8 @@
 import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
 import androidx.camera.core.impl.CameraCaptureMetaData.FlashState;
 import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
+import androidx.exifinterface.media.ExifInterface;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -36,13 +42,15 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import java.util.concurrent.TimeUnit;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public final class Camera2CameraCaptureResultTest {
 
     private CaptureResult mCaptureResult;
     private Camera2CameraCaptureResult mCamera2CameraCaptureResult;
-    private TagBundle mTag = TagBundle.emptyBundle();
+    private final TagBundle mTag = TagBundle.emptyBundle();
 
     @Before
     public void setUp() {
@@ -275,4 +283,74 @@
                 .thenReturn(CaptureResult.FLASH_STATE_PARTIAL);
         assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
     }
+
+    @Test
+    public void canPopulateExif() {
+        // Arrange
+        when(mCaptureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_FIRED);
+
+        Rect cropRegion = new Rect(0, 0, 640, 480);
+        when(mCaptureResult.get(CaptureResult.SCALER_CROP_REGION)).thenReturn(cropRegion);
+
+        when(mCaptureResult.get(CaptureResult.JPEG_ORIENTATION)).thenReturn(270);
+
+        long exposureTime = TimeUnit.SECONDS.toNanos(5);
+        when(mCaptureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME)).thenReturn(exposureTime);
+
+        float aperture = 1.8f;
+        when(mCaptureResult.get(CaptureResult.LENS_APERTURE)).thenReturn(aperture);
+
+        int iso = 200;
+        int postRawSensitivityBoost = 200;
+        when(mCaptureResult.get(CaptureResult.SENSOR_SENSITIVITY)).thenReturn(iso);
+        when(mCaptureResult.get(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST))
+                .thenReturn(postRawSensitivityBoost);
+
+        float focalLength = 4200f;
+        when(mCaptureResult.get(CaptureResult.LENS_FOCAL_LENGTH)).thenReturn(focalLength);
+
+        when(mCaptureResult.get(CaptureResult.CONTROL_AWB_MODE))
+                .thenReturn(CameraMetadata.CONTROL_AWB_MODE_OFF);
+
+        // Act
+        ExifData.Builder exifBuilder = ExifData.builderForDevice();
+        mCamera2CameraCaptureResult.populateExifData(exifBuilder);
+        ExifData exifData = exifBuilder.build();
+
+        // Assert
+        assertThat(Short.parseShort(exifData.getAttribute(ExifInterface.TAG_FLASH)))
+                .isEqualTo(FLAG_FLASH_FIRED);
+
+        assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_WIDTH))
+                .isEqualTo(String.valueOf(cropRegion.width()));
+
+        assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_LENGTH))
+                .isEqualTo(String.valueOf(cropRegion.height()));
+
+        assertThat(exifData.getAttribute(ExifInterface.TAG_ORIENTATION))
+                .isEqualTo(String.valueOf(ExifInterface.ORIENTATION_ROTATE_270));
+
+        String exposureTimeString = exifData.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
+        assertThat(exposureTimeString).isNotNull();
+        assertThat(Float.parseFloat(exposureTimeString)).isWithin(0.1f)
+                .of(TimeUnit.NANOSECONDS.toSeconds(exposureTime));
+
+        assertThat(exifData.getAttribute(ExifInterface.TAG_F_NUMBER))
+                .isEqualTo(String.valueOf(aperture));
+
+        assertThat(
+                Short.parseShort(exifData.getAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY)))
+                .isEqualTo((short) (iso * (int) (postRawSensitivityBoost / 100f)));
+
+        String focalLengthString = exifData.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
+        assertThat(focalLengthString).isNotNull();
+        String[] fractionValues = focalLengthString.split("/");
+        long numerator = Long.parseLong(fractionValues[0]);
+        long denominator = Long.parseLong(fractionValues[1]);
+        assertThat(numerator / (float) denominator).isWithin(0.1f).of(focalLength);
+
+        assertThat(Short.parseShort(exifData.getAttribute(ExifInterface.TAG_WHITE_BALANCE)))
+                .isEqualTo(ExifInterface.WHITE_BALANCE_MANUAL);
+    }
 }
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 633e483..de75e09 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -46,14 +46,12 @@
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraFactory;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CameraStateRegistry;
-import androidx.camera.core.impl.CameraThreadConfig;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.ImmediateSurface;
@@ -112,7 +110,6 @@
             CameraInternal.State.OPEN,
             CameraInternal.State.RELEASED));
 
-    private static CameraFactory sCameraFactory;
     static ExecutorService sCameraExecutor;
 
     @Rule
@@ -130,13 +127,11 @@
     SemaphoreReleasingCamera2Callbacks.SessionStateCallback mSessionStateCallback;
 
     @BeforeClass
-    public static void classSetup() {
+    public static void classSetup() throws InitializationException {
         sCameraHandlerThread = new HandlerThread("cameraThread");
         sCameraHandlerThread.start();
         sCameraHandler = HandlerCompat.createAsync(sCameraHandlerThread.getLooper());
         sCameraExecutor = CameraXExecutors.newHandlerExecutor(sCameraHandler);
-        sCameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext(),
-                CameraThreadConfig.create(sCameraExecutor, sCameraHandler));
     }
 
     @AfterClass
@@ -145,14 +140,18 @@
     }
 
     @Before
-    public void setup() throws CameraUnavailableException {
+    public void setup() throws Exception {
         mMockOnImageAvailableListener = Mockito.mock(ImageReader.OnImageAvailableListener.class);
         mSessionStateCallback = new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
         mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
         mSemaphore = new Semaphore(0);
         mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
-        mCamera2CameraImpl = new Camera2CameraImpl(
-                CameraManagerCompat.from(ApplicationProvider.getApplicationContext()), mCameraId,
+        CameraManagerCompat cameraManagerCompat =
+                CameraManagerCompat.from(ApplicationProvider.getApplicationContext());
+        Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
+                mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
+        mCamera2CameraImpl = new Camera2CameraImpl(cameraManagerCompat, mCameraId,
+                camera2CameraInfo,
                 mCameraStateRegistry, sCameraExecutor, sCameraHandler);
     }
 
@@ -513,7 +512,7 @@
     @Test
     public void cameraTransitionsThroughPendingState_whenNoCamerasAvailable() {
         @SuppressWarnings("unchecked") // Cannot mock generic type inline
-                Observable.Observer<CameraInternal.State> mockObserver =
+        Observable.Observer<CameraInternal.State> mockObserver =
                 mock(Observable.Observer.class);
 
         // Ensure real camera can't open due to max cameras being open
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index 06b16972..a053541 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -45,7 +45,6 @@
 import androidx.camera.camera2.internal.util.SemaphoreReleasingCamera2Callbacks;
 import androidx.camera.camera2.interop.Camera2Interop;
 import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.ExperimentalExposureCompensation;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.ImageCapture;
@@ -132,7 +131,7 @@
     }
 
     @Before
-    public void setup() throws CameraUnavailableException {
+    public void setup() throws Exception {
         // TODO(b/162296654): Workaround the google_3a specific behavior.
         assumeFalse("Cuttlefish uses google_3a v1 or v2 it might fail to set EV before "
                 + "first AE converge.", android.os.Build.MODEL.contains("Cuttlefish"));
@@ -147,8 +146,13 @@
         mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
         mSemaphore = new Semaphore(0);
         mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
+        CameraManagerCompat cameraManagerCompat =
+                CameraManagerCompat.from(ApplicationProvider.getApplicationContext());
+        Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
+                mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
         mCamera2CameraImpl = new Camera2CameraImpl(
                 CameraManagerCompat.from(ApplicationProvider.getApplicationContext()), mCameraId,
+                camera2CameraInfo,
                 mCameraStateRegistry, sCameraExecutor, sCameraHandler);
 
         mCameraInfoInternal = mCamera2CameraImpl.getCameraInfoInternal();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
index 4b02c8f..0df28bd 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
@@ -47,9 +47,10 @@
 
         // Create the DeviceSurfaceManager for Camera2
         CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
-                (context, cameraManager) -> {
+                (context, cameraManager, availableCameraIds) -> {
                     try {
-                        return new Camera2DeviceSurfaceManager(context, cameraManager);
+                        return new Camera2DeviceSurfaceManager(context, cameraManager,
+                                availableCameraIds);
                     } catch (CameraUnavailableException e) {
                         throw new InitializationException(e);
                     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java
index c4a51cc..928d8ef 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java
@@ -16,7 +16,10 @@
 
 package androidx.camera.camera2.internal;
 
+import android.graphics.Rect;
+import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.CaptureResult;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.camera.core.Logger;
@@ -27,6 +30,7 @@
 import androidx.camera.core.impl.CameraCaptureMetaData.FlashState;
 import androidx.camera.core.impl.CameraCaptureResult;
 import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
 
 /** The camera2 implementation for the capture result of a single image capture. */
 public class Camera2CameraCaptureResult implements CameraCaptureResult {
@@ -203,6 +207,66 @@
         return mTagBundle;
     }
 
+    @Override
+    public void populateExifData(@NonNull ExifData.Builder exifData) {
+        // Call interface default to set flash mode
+        CameraCaptureResult.super.populateExifData(exifData);
+
+        // Set dimensions
+        Rect cropRegion = mCaptureResult.get(CaptureResult.SCALER_CROP_REGION);
+        if (cropRegion != null) {
+            exifData.setImageWidth(cropRegion.width())
+                    .setImageHeight(cropRegion.height());
+        }
+
+        // Set orientation
+        Integer jpegOrientation = mCaptureResult.get(CaptureResult.JPEG_ORIENTATION);
+        if (jpegOrientation != null) {
+            exifData.setOrientationDegrees(jpegOrientation);
+        }
+
+        // Set exposure time
+        Long exposureTimeNs = mCaptureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME);
+        if (exposureTimeNs != null) {
+            exifData.setExposureTimeNanos(exposureTimeNs);
+        }
+
+        // Set the aperture
+        Float aperture = mCaptureResult.get(CaptureResult.LENS_APERTURE);
+        if (aperture != null) {
+            exifData.setLensFNumber(aperture);
+        }
+
+        // Set the ISO
+        Integer iso = mCaptureResult.get(CaptureResult.SENSOR_SENSITIVITY);
+        if (iso != null) {
+            if (Build.VERSION.SDK_INT >= 24) {
+                Integer postRawSensitivityBoost =
+                        mCaptureResult.get(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST);
+                if (postRawSensitivityBoost != null) {
+                    iso *= (int) (postRawSensitivityBoost / 100f);
+                }
+            }
+            exifData.setIso(iso);
+        }
+
+        // Set the focal length
+        Float focalLength = mCaptureResult.get(CaptureResult.LENS_FOCAL_LENGTH);
+        if (focalLength != null) {
+            exifData.setFocalLength(focalLength);
+        }
+
+        // Set white balance MANUAL/AUTO
+        Integer whiteBalanceMode = mCaptureResult.get(CaptureResult.CONTROL_AWB_MODE);
+        if (whiteBalanceMode != null) {
+            ExifData.WhiteBalanceMode wbMode = ExifData.WhiteBalanceMode.AUTO;
+            if (whiteBalanceMode == CameraMetadata.CONTROL_AWB_MODE_OFF) {
+                wbMode = ExifData.WhiteBalanceMode.MANUAL;
+            }
+            exifData.setWhiteBalanceMode(wbMode);
+        }
+    }
+
     @NonNull
     public CaptureResult getCaptureResult() {
         return mCaptureResult;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
index 2236387..61c4069 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
@@ -19,17 +19,21 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
 import androidx.camera.core.impl.CameraFactory;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CameraStateRegistry;
 import androidx.camera.core.impl.CameraThreadConfig;
 
-import java.util.Arrays;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -38,40 +42,57 @@
 public final class Camera2CameraFactory implements CameraFactory {
     private static final int DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS = 1;
     private final CameraThreadConfig mThreadConfig;
-
     private final CameraStateRegistry mCameraStateRegistry;
     private final CameraManagerCompat mCameraManager;
+    private final List<String> mAvailableCameraIds;
+    private final Map<String, Camera2CameraInfoImpl> mCameraInfos = new HashMap<>();
 
     /** Creates a Camera2 implementation of CameraFactory */
     public Camera2CameraFactory(@NonNull Context context,
-            @NonNull CameraThreadConfig threadConfig) {
+            @NonNull CameraThreadConfig threadConfig,
+            @Nullable CameraSelector availableCamerasSelector) throws InitializationException {
         mThreadConfig = threadConfig;
         mCameraStateRegistry = new CameraStateRegistry(DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS);
         mCameraManager = CameraManagerCompat.from(context, mThreadConfig.getSchedulerHandler());
+
+        mAvailableCameraIds = CameraSelectionOptimizer
+                .getSelectedAvailableCameraIds(this, availableCamerasSelector);
     }
 
     @Override
     @NonNull
     public CameraInternal getCamera(@NonNull String cameraId) throws CameraUnavailableException {
-        if (!getAvailableCameraIds().contains(cameraId)) {
+        if (!mAvailableCameraIds.contains(cameraId)) {
             throw new IllegalArgumentException(
                     "The given camera id is not on the available camera id list.");
         }
-        return new Camera2CameraImpl(mCameraManager, cameraId, mCameraStateRegistry,
-                mThreadConfig.getCameraExecutor(), mThreadConfig.getSchedulerHandler());
+        return new Camera2CameraImpl(mCameraManager,
+                cameraId,
+                getCameraInfo(cameraId),
+                mCameraStateRegistry,
+                mThreadConfig.getCameraExecutor(),
+                mThreadConfig.getSchedulerHandler());
     }
 
-    @Override
-    @NonNull
-    public Set<String> getAvailableCameraIds() throws CameraUnavailableException {
-        List<String> camerasList;
+    Camera2CameraInfoImpl getCameraInfo(@NonNull String cameraId)
+            throws CameraUnavailableException {
         try {
-            camerasList = Arrays.asList(mCameraManager.getCameraIdList());
+            Camera2CameraInfoImpl camera2CameraInfoImpl = mCameraInfos.get(cameraId);
+            if (camera2CameraInfoImpl == null) {
+                camera2CameraInfoImpl = new Camera2CameraInfoImpl(
+                        cameraId, mCameraManager.getCameraCharacteristicsCompat(cameraId));
+                mCameraInfos.put(cameraId, camera2CameraInfoImpl);
+            }
+            return camera2CameraInfoImpl;
         } catch (CameraAccessExceptionCompat e) {
             throw CameraUnavailableExceptionHelper.createFrom(e);
         }
+    }
+    @Override
+    @NonNull
+    public Set<String> getAvailableCameraIds() {
         // Use a LinkedHashSet to preserve order
-        return new LinkedHashSet<>(camerasList);
+        return new LinkedHashSet<>(mAvailableCameraIds);
     }
 
     @NonNull
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 3cd5d0d..c3df3e65 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -37,7 +37,6 @@
 import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
-import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
@@ -51,7 +50,6 @@
 import androidx.camera.core.impl.ImmediateSurface;
 import androidx.camera.core.impl.LiveDataObservable;
 import androidx.camera.core.impl.Observable;
-import androidx.camera.core.impl.Quirks;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.SessionConfig.ValidatingBuilder;
 import androidx.camera.core.impl.UseCaseAttachState;
@@ -168,9 +166,6 @@
     private final SynchronizedCaptureSessionOpener.Builder mCaptureSessionOpenerBuilder;
     private final Set<String> mNotifyStateAttachedSet = new HashSet<>();
 
-    @NonNull
-    private final Quirks mCameraQuirks;
-
     /**
      * Constructor for a camera.
      *
@@ -185,6 +180,7 @@
      */
     Camera2CameraImpl(@NonNull CameraManagerCompat cameraManager,
             @NonNull String cameraId,
+            @NonNull Camera2CameraInfoImpl cameraInfoImpl,
             @NonNull CameraStateRegistry cameraStateRegistry,
             @NonNull Executor executor,
             @NonNull Handler schedulerHandler) throws CameraUnavailableException {
@@ -202,14 +198,11 @@
         try {
             CameraCharacteristicsCompat cameraCharacteristicsCompat =
                     mCameraManager.getCameraCharacteristicsCompat(cameraId);
-            mCameraQuirks = CameraQuirks.get(cameraId, cameraCharacteristicsCompat);
             mCameraControlInternal = new Camera2CameraControlImpl(cameraCharacteristicsCompat,
                     executorScheduler, mExecutor, new ControlUpdateListenerInternal(),
-                    mCameraQuirks);
-            mCameraInfoInternal = new Camera2CameraInfoImpl(
-                    cameraId,
-                    cameraCharacteristicsCompat,
-                    mCameraControlInternal);
+                    cameraInfoImpl.getCameraQuirks());
+            mCameraInfoInternal = cameraInfoImpl;
+            mCameraInfoInternal.linkWithCameraControl(mCameraControlInternal);
         } catch (CameraAccessExceptionCompat e) {
             throw CameraUnavailableExceptionHelper.createFrom(e);
         }
@@ -881,13 +874,6 @@
         return mCameraInfoInternal;
     }
 
-    /** {@inheritDoc} */
-    @NonNull
-    @Override
-    public Quirks getCameraQuirks() {
-        return mCameraQuirks;
-    }
-
     /** Opens the camera device */
     // TODO(b/124268878): Handle SecurityException and require permission in manifest.
     @SuppressLint("MissingPermission")
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index ccea681..b31c41c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -18,12 +18,15 @@
 
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraMetadata;
+import android.util.Pair;
 import android.view.Surface;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraSelector;
@@ -34,15 +37,28 @@
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.Quirks;
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
 
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
  * Implementation of the {@link CameraInfoInternal} interface that exposes parameters through
  * camera2.
+ *
+ * <p>Construction consists of two stages. The constructor creates a implementation without a
+ * {@link Camera2CameraControlImpl} and will return default values for camera control related
+ * states like zoom/exposure/torch. After {@link #linkWithCameraControl} is called,
+ * zoom/exposure/torch API will reflect the states in the {@link Camera2CameraControlImpl}. Any
+ * CameraCaptureCallbacks added before this link will also be added
+ * to the {@link Camera2CameraControlImpl}.
  */
 @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
 public final class Camera2CameraInfoImpl implements CameraInfoInternal {
@@ -50,22 +66,66 @@
     private static final String TAG = "Camera2CameraInfo";
     private final String mCameraId;
     private final CameraCharacteristicsCompat mCameraCharacteristicsCompat;
-    private final Camera2CameraControlImpl mCamera2CameraControlImpl;
-    private final ZoomControl mZoomControl;
-    private final TorchControl mTorchControl;
-    private final ExposureControl mExposureControl;
     private final Camera2CameraInfo mCamera2CameraInfo;
 
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    @Nullable
+    private Camera2CameraControlImpl mCamera2CameraControlImpl;
+    @GuardedBy("mLock")
+    @Nullable
+    private RedirectableLiveData<Integer> mRedirectTorchStateLiveData = null;
+    @GuardedBy("mLock")
+    @Nullable
+    private RedirectableLiveData<ZoomState> mRedirectZoomStateLiveData = null;
+    @GuardedBy("mLock")
+    @Nullable
+    private List<Pair<CameraCaptureCallback, Executor>> mCameraCaptureCallbacks = null;
+
+    @NonNull
+    private final Quirks mCameraQuirks;
+
+    /**
+     * Constructs an instance. Before {@link #linkWithCameraControl(Camera2CameraControlImpl)} is
+     * called, camera control related API (torch/exposure/zoom) will return default values.
+     */
     Camera2CameraInfoImpl(@NonNull String cameraId,
-            @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat,
-            @NonNull Camera2CameraControlImpl camera2CameraControlImpl) {
+            @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
         mCameraId = Preconditions.checkNotNull(cameraId);
         mCameraCharacteristicsCompat = cameraCharacteristicsCompat;
-        mCamera2CameraControlImpl = camera2CameraControlImpl;
-        mZoomControl = camera2CameraControlImpl.getZoomControl();
-        mTorchControl = camera2CameraControlImpl.getTorchControl();
-        mExposureControl = camera2CameraControlImpl.getExposureControl();
         mCamera2CameraInfo = new Camera2CameraInfo(this);
+        mCameraQuirks = CameraQuirks.get(cameraId, cameraCharacteristicsCompat);
+    }
+
+    /**
+     * Links with a {@link Camera2CameraControlImpl}. After the link, zoom/torch/exposure
+     * operations of CameraControl will modify the states in this Camera2CameraInfoImpl.
+     * Also, any CameraCaptureCallbacks added before this link will be added to the
+     * {@link Camera2CameraControlImpl}.
+     */
+    void linkWithCameraControl(@NonNull Camera2CameraControlImpl camera2CameraControlImpl) {
+        synchronized (mLock) {
+            mCamera2CameraControlImpl = camera2CameraControlImpl;
+
+            if (mRedirectZoomStateLiveData != null) {
+                mRedirectZoomStateLiveData.redirectTo(
+                        mCamera2CameraControlImpl.getZoomControl().getZoomState());
+            }
+
+            if (mRedirectTorchStateLiveData != null) {
+                mRedirectTorchStateLiveData.redirectTo(
+                        mCamera2CameraControlImpl.getTorchControl().getTorchState());
+            }
+
+            if (mCameraCaptureCallbacks != null) {
+                for (Pair<CameraCaptureCallback, Executor> pair :
+                        mCameraCaptureCallbacks) {
+                    mCamera2CameraControlImpl.addSessionCameraCaptureCallback(pair.second,
+                            pair.first);
+                }
+                mCameraCaptureCallbacks = null;
+            }
+        }
         logDeviceInfo();
     }
 
@@ -175,20 +235,55 @@
     @NonNull
     @Override
     public LiveData<Integer> getTorchState() {
-        return mTorchControl.getTorchState();
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mRedirectTorchStateLiveData == null) {
+                    mRedirectTorchStateLiveData =
+                            new RedirectableLiveData<>(TorchControl.DEFAULT_TORCH_STATE);
+                }
+                return mRedirectTorchStateLiveData;
+            }
+
+            // if RedirectableLiveData exists,  use it directly.
+            if (mRedirectTorchStateLiveData != null) {
+                return mRedirectTorchStateLiveData;
+            }
+
+            return mCamera2CameraControlImpl.getTorchControl().getTorchState();
+        }
     }
 
     @NonNull
     @Override
     public LiveData<ZoomState> getZoomState() {
-        return mZoomControl.getZoomState();
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mRedirectZoomStateLiveData == null) {
+                    mRedirectZoomStateLiveData = new RedirectableLiveData<>(
+                            ZoomControl.getDefaultZoomState(mCameraCharacteristicsCompat));
+                }
+                return mRedirectZoomStateLiveData;
+            }
+
+            // if RedirectableLiveData exists,  use it directly.
+            if (mRedirectZoomStateLiveData != null) {
+                return mRedirectZoomStateLiveData;
+            }
+
+            return mCamera2CameraControlImpl.getZoomControl().getZoomState();
+        }
     }
 
     @NonNull
     @Override
     @ExperimentalExposureCompensation
     public ExposureState getExposureState() {
-        return mExposureControl.getExposureState();
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                return ExposureControl.getDefaultExposureState(mCameraCharacteristicsCompat);
+            }
+            return mCamera2CameraControlImpl.getExposureControl().getExposureState();
+        }
     }
 
     /**
@@ -213,12 +308,45 @@
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
-        mCamera2CameraControlImpl.addSessionCameraCaptureCallback(executor, callback);
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mCameraCaptureCallbacks == null) {
+                    mCameraCaptureCallbacks = new ArrayList<>();
+                }
+                mCameraCaptureCallbacks.add(new Pair<>(callback, executor));
+                return;
+            }
+
+            mCamera2CameraControlImpl.addSessionCameraCaptureCallback(executor, callback);
+        }
     }
 
     @Override
     public void removeSessionCaptureCallback(@NonNull CameraCaptureCallback callback) {
-        mCamera2CameraControlImpl.removeSessionCameraCaptureCallback(callback);
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mCameraCaptureCallbacks == null) {
+                    return;
+                }
+                Iterator<Pair<CameraCaptureCallback, Executor>> it =
+                        mCameraCaptureCallbacks.iterator();
+                while (it.hasNext()) {
+                    Pair<CameraCaptureCallback, Executor> pair = it.next();
+                    if (pair.first == callback) {
+                        it.remove();
+                    }
+                }
+                return;
+            }
+            mCamera2CameraControlImpl.removeSessionCameraCaptureCallback(callback);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NonNull
+    @Override
+    public Quirks getCameraQuirks() {
+        return mCameraQuirks;
     }
 
     /**
@@ -228,4 +356,40 @@
     public Camera2CameraInfo getCamera2CameraInfo() {
         return mCamera2CameraInfo;
     }
+
+    /**
+     * A {@link LiveData} which can be redirected to another {@link LiveData}. If no redirection
+     * is set, initial value will be used.
+     */
+    static class RedirectableLiveData<T> extends MediatorLiveData<T> {
+        private LiveData<T> mLiveDataSource;
+        private T mInitialValue;
+
+        RedirectableLiveData(T initialValue) {
+            mInitialValue = initialValue;
+        }
+
+        void redirectTo(@NonNull LiveData<T> liveDataSource) {
+            if (mLiveDataSource != null) {
+                super.removeSource(mLiveDataSource);
+            }
+            mLiveDataSource = liveDataSource;
+            super.addSource(liveDataSource, this::setValue);
+        }
+
+        @Override
+        public <S> void addSource(@NonNull LiveData<S> source,
+                @NonNull Observer<? super S> onChanged) {
+            throw new UnsupportedOperationException();
+        }
+
+        // Overrides getValue() to reflect the correct value from source. This is required to ensure
+        // getValue() is correct when observe() or observeForever() is not called.
+        @Override
+        public T getValue() {
+            // Returns initial value if source is not set.
+            return mLiveDataSource == null ? mInitialValue : mLiveDataSource.getValue();
+        }
+    }
+
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index e5eb1ff..9479bf5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -24,7 +24,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
-import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
@@ -36,6 +35,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Camera device manager to provide the guaranteed supported stream capabilities related info for
@@ -60,13 +60,15 @@
      */
     @RestrictTo(Scope.LIBRARY)
     public Camera2DeviceSurfaceManager(@NonNull Context context,
-            @Nullable Object cameraManager) throws CameraUnavailableException {
-        this(context, CamcorderProfile::hasProfile, cameraManager);
+            @Nullable Object cameraManager, @NonNull Set<String> availableCameraIds)
+            throws CameraUnavailableException {
+        this(context, CamcorderProfile::hasProfile, cameraManager, availableCameraIds);
     }
 
     Camera2DeviceSurfaceManager(@NonNull Context context,
             @NonNull CamcorderProfileHelper camcorderProfileHelper,
-            @Nullable Object cameraManager)
+            @Nullable Object cameraManager,
+            @NonNull Set<String> availableCameraIds)
             throws CameraUnavailableException {
         Preconditions.checkNotNull(camcorderProfileHelper);
         mCamcorderProfileHelper = camcorderProfileHelper;
@@ -77,25 +79,22 @@
         } else {
             cameraManagerCompat = CameraManagerCompat.from(context);
         }
-        init(context, cameraManagerCompat);
+        init(context, cameraManagerCompat, availableCameraIds);
     }
 
     /**
      * Prepare necessary resources for the surface manager.
      */
-    private void init(@NonNull Context context, @NonNull CameraManagerCompat cameraManager)
+    private void init(@NonNull Context context, @NonNull CameraManagerCompat cameraManager,
+            @NonNull Set<String> availableCameraIds)
             throws CameraUnavailableException {
         Preconditions.checkNotNull(context);
 
-        try {
-            for (String cameraId : cameraManager.getCameraIdList()) {
-                mCameraSupportedSurfaceCombinationMap.put(
-                        cameraId,
-                        new SupportedSurfaceCombination(
-                                context, cameraId, cameraManager, mCamcorderProfileHelper));
-            }
-        } catch (CameraAccessExceptionCompat e) {
-            throw CameraUnavailableExceptionHelper.createFrom(e);
+        for (String cameraId : availableCameraIds) {
+            mCameraSupportedSurfaceCombinationMap.put(
+                    cameraId,
+                    new SupportedSurfaceCombination(
+                            context, cameraId, cameraManager, mCamcorderProfileHelper));
         }
     }
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
index 2d6d755..703e6a1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
@@ -42,6 +42,7 @@
 class CameraBurstCaptureCallback extends CameraCaptureSession.CaptureCallback {
 
     final Map<CaptureRequest, List<CameraCaptureSession.CaptureCallback>> mCallbackMap;
+    CaptureSequenceCallback mCaptureSequenceCallback = null;
 
     CameraBurstCaptureCallback() {
         mCallbackMap = new HashMap<>();
@@ -98,13 +99,18 @@
     @Override
     public void onCaptureSequenceAborted(
             @NonNull CameraCaptureSession session, int sequenceId) {
-        // No-op.
+        if (mCaptureSequenceCallback != null) {
+            mCaptureSequenceCallback.onCaptureSequenceCompletedOrAborted(session, sequenceId, true);
+        }
     }
 
     @Override
     public void onCaptureSequenceCompleted(
             @NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
-        // No-op.
+        if (mCaptureSequenceCallback != null) {
+            mCaptureSequenceCallback.onCaptureSequenceCompletedOrAborted(session, sequenceId,
+                    false);
+        }
     }
 
     private List<CameraCaptureSession.CaptureCallback> getCallbacks(CaptureRequest request) {
@@ -131,4 +137,20 @@
         }
     }
 
+    /**
+     * Sets the callback to receive the notification when the capture sequence is completed or
+     * aborted.
+     */
+    public void setCaptureSequenceCallback(@NonNull CaptureSequenceCallback callback) {
+        mCaptureSequenceCallback = callback;
+    }
+
+    /**
+     * A interface to receive the notification of onCaptureSequenceCompleted or
+     * onCaptureSequenceAborted.
+     */
+    interface CaptureSequenceCallback {
+        void onCaptureSequenceCompletedOrAborted(
+                @NonNull CameraCaptureSession session, int sequenceId, boolean isAborted);
+    }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java
new file mode 100644
index 0000000..d0a146f
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraMetadata;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
+import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
+import androidx.camera.core.impl.CameraInfoInternal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class CameraSelectionOptimizer {
+    private CameraSelectionOptimizer() {
+    }
+
+    static List<String> getSelectedAvailableCameraIds(
+            @NonNull Camera2CameraFactory cameraFactory,
+            @Nullable CameraSelector availableCamerasSelector)
+            throws InitializationException {
+        try {
+            List<String> availableCameraIds = new ArrayList<>();
+            String[] cameraIdList = cameraFactory.getCameraManager().getCameraIdList();
+            if (availableCamerasSelector == null) {
+                for (String id : cameraIdList) {
+                    availableCameraIds.add(id);
+                }
+                return availableCameraIds;
+            }
+
+            // Skip camera ID by heuristic: 0 is back lens facing, 1 is front lens facing.
+            Integer lensFacingInteger = availableCamerasSelector.getLensFacing();
+            String skippedCameraId = decideSkippedCameraIdByHeuristic(
+                    cameraFactory.getCameraManager(), lensFacingInteger);
+            List<CameraInfo> cameraInfos = new ArrayList<>();
+
+            for (String id : cameraIdList) {
+                if (id.equals(skippedCameraId)) {
+                    continue;
+                }
+                Camera2CameraInfoImpl cameraInfo = cameraFactory.getCameraInfo(id);
+                cameraInfos.add(cameraInfo);
+            }
+
+            List<CameraInfo> filteredCameraInfos =
+                    availableCamerasSelector.filter(cameraInfos);
+
+            for (CameraInfo cameraInfo : filteredCameraInfos) {
+                String cameraId = ((CameraInfoInternal) cameraInfo).getCameraId();
+                availableCameraIds.add(cameraId);
+            }
+
+            return availableCameraIds;
+        } catch (CameraAccessExceptionCompat e) {
+            throw new InitializationException(CameraUnavailableExceptionHelper.createFrom(e));
+        } catch (CameraUnavailableException e) {
+            throw new InitializationException(e);
+        }
+    }
+
+    // Returns the camera id that can be safely skipped.
+    // Returns null if no camera ids can be skipped.
+    private static String decideSkippedCameraIdByHeuristic(CameraManagerCompat cameraManager,
+            Integer lensFacingInteger) throws CameraAccessExceptionCompat {
+        String skippedCameraId = null;
+        if (lensFacingInteger == null) { // Not specifying lens facing,  cannot skip any camera id.
+            return null;
+        } else if (lensFacingInteger.intValue() == CameraSelector.LENS_FACING_BACK) {
+            if (cameraManager.getCameraCharacteristicsCompat("0").get(
+                    CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK) {
+                // If apps requires back lens facing,  and "0" is confirmed to be back
+                // We can safely ignore "1" as a optimization for initialization latency
+                skippedCameraId = "1";
+            }
+        } else if (lensFacingInteger.intValue() == CameraSelector.LENS_FACING_FRONT) {
+            if (cameraManager.getCameraCharacteristicsCompat("1").get(
+                    CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT) {
+                // If apps requires front lens facing,  and "1" is confirmed to be back
+                // We can safely ignore "0" as a optimization for initialization latency
+                skippedCameraId = "0";
+            }
+        }
+
+        return skippedCameraId;
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 7418b99..f61d60e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -32,6 +32,7 @@
 import androidx.camera.camera2.impl.CameraEventCallbacks;
 import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
 import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
+import androidx.camera.camera2.internal.compat.workaround.StillCaptureFlow;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.CameraCaptureCallback;
@@ -120,6 +121,7 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @GuardedBy("mStateLock")
     CallbackToFutureAdapter.Completer<Void> mReleaseCompleter;
+    final StillCaptureFlow mStillCaptureFlow = new StillCaptureFlow();
 
     /**
      * Constructor for CaptureSession.
@@ -633,6 +635,7 @@
         try {
             CameraBurstCaptureCallback callbackAggregator = new CameraBurstCaptureCallback();
             List<CaptureRequest> captureRequests = new ArrayList<>();
+            boolean isStillCapture = false;
             Logger.d(TAG, "Issuing capture request.");
             for (CaptureConfig captureConfig : captureConfigs) {
                 if (captureConfig.getSurfaces().isEmpty()) {
@@ -657,6 +660,9 @@
                     continue;
                 }
 
+                if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_STILL_CAPTURE) {
+                    isStillCapture = true;
+                }
                 CaptureConfig.Builder captureConfigBuilder = CaptureConfig.Builder.from(
                         captureConfig);
 
@@ -692,6 +698,18 @@
             }
 
             if (!captureRequests.isEmpty()) {
+                if (mStillCaptureFlow
+                        .shouldStopRepeatingBeforeCapture(captureRequests, isStillCapture)) {
+                    mSynchronizedCaptureSession.stopRepeating();
+                    callbackAggregator.setCaptureSequenceCallback(
+                            (session, sequenceId, isAborted) -> {
+                                synchronized (mStateLock) {
+                                    if (mState == State.OPENED) {
+                                        issueRepeatingCaptureRequests();
+                                    }
+                                }
+                            });
+                }
                 mSynchronizedCaptureSession.captureBurstRequests(captureRequests,
                         callbackAggregator);
             } else {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
index 9b8e776..12e6f7f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
@@ -93,6 +93,11 @@
         mExecutor = executor;
     }
 
+    static ExposureState getDefaultExposureState(
+            CameraCharacteristicsCompat cameraCharacteristics) {
+        return new ExposureStateImpl(cameraCharacteristics, DEFAULT_EXPOSURE_COMPENSATION);
+    }
+
     /**
      * Set current active state. Set active if it is ready to accept operations.
      *
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
index 13fdc47..bf44fdf 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
@@ -75,6 +75,8 @@
     public boolean isExposureCompensationSupported() {
         Range<Integer> compensationRange =
                 mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
-        return compensationRange.getLower() != 0 && compensationRange.getUpper() != 0;
+        return compensationRange != null
+                && compensationRange.getLower() != 0
+                && compensationRange.getUpper() != 0;
     }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index 7c86acb..695b9bc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -48,6 +48,7 @@
  */
 final class TorchControl {
     private static final String TAG = "TorchControl";
+    static final int DEFAULT_TORCH_STATE = TorchState.OFF;
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     private final Camera2CameraControlImpl mCamera2CameraControlImpl;
@@ -77,7 +78,7 @@
         Boolean hasFlashUnit =
                 cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
         mHasFlashUnit = hasFlashUnit != null && hasFlashUnit.booleanValue();
-        mTorchState = new MutableLiveData<>(TorchState.OFF);
+        mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
         mCamera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
     }
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
index 7d79583..888e0b2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
@@ -98,7 +98,15 @@
         camera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
     }
 
-    private ZoomImpl createZoomImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
+    static ZoomState getDefaultZoomState(CameraCharacteristicsCompat cameraCharacteristics) {
+        ZoomImpl zoomImpl = createZoomImpl(cameraCharacteristics);
+        ZoomStateImpl zoomState = new ZoomStateImpl(zoomImpl.getMaxZoom(), zoomImpl.getMinZoom());
+        zoomState.setZoomRatio(DEFAULT_ZOOM_RATIO);
+        return ImmutableZoomState.create(zoomState);
+    }
+
+    private static ZoomImpl createZoomImpl(
+            @NonNull CameraCharacteristicsCompat cameraCharacteristics) {
         if (isAndroidRZoomSupported(cameraCharacteristics)) {
             return new AndroidRZoomImpl(cameraCharacteristics);
         } else {
@@ -106,7 +114,8 @@
         }
     }
 
-    private boolean isAndroidRZoomSupported(CameraCharacteristicsCompat cameraCharacteristics) {
+    private static boolean isAndroidRZoomSupported(
+            CameraCharacteristicsCompat cameraCharacteristics) {
         return Build.VERSION.SDK_INT >= 30 && cameraCharacteristics.get(
                 CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) != null;
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index ba59291..374c616 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -57,6 +57,9 @@
         if (PreviewPixelHDRnetQuirk.load()) {
             quirks.add(new PreviewPixelHDRnetQuirk());
         }
+        if (StillCaptureFlashStopRepeatingQuirk.load()) {
+            quirks.add(new StillCaptureFlashStopRepeatingQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java
new file mode 100644
index 0000000..bc50f27
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Locale;
+
+/**
+ * Quirk that still capture with flash on/auto requires stopRepeating() being called ahead of
+ * capture.
+ *
+ * <p>On some devices like Samsung SM-A716B, it could lead to CaptureRequest not being completed
+ * when taking photos in dark environment with flash on/auto. Calling stopRepeating ahead of
+ * still capture and setRepeating again after capture is done can fix the issue. See b/172036589.
+ */
+public class StillCaptureFlashStopRepeatingQuirk implements Quirk {
+    static boolean load() {
+        return "SAMSUNG".equals(Build.MANUFACTURER.toUpperCase(Locale.US))
+                // Enables it on all A716 models.
+                && android.os.Build.MODEL.toUpperCase(Locale.US).startsWith("SM-A716");
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java
new file mode 100644
index 0000000..7a22956
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.workaround;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.StillCaptureFlashStopRepeatingQuirk;
+
+import java.util.List;
+
+/**
+ * Workaround to fix device issues such as calling stopRepeating ahead of still
+ * capture on some devices when flash is on or auto. See b/172036589.
+ */
+public class StillCaptureFlow {
+    private final boolean mShouldStopRepeatingBeforeStillCapture;
+    public StillCaptureFlow() {
+        final StillCaptureFlashStopRepeatingQuirk quirk = DeviceQuirks.get(
+                StillCaptureFlashStopRepeatingQuirk.class);
+
+        mShouldStopRepeatingBeforeStillCapture = (quirk != null);
+    }
+
+    /**
+     * Returns whether or not it should call stopRepeating ahead of capture request.
+     *
+     * @param captureRequests captureRequests to be executed
+     * @param isStillCapture true if captureRequests contain a still capture request.
+     * @return
+     */
+    public boolean shouldStopRepeatingBeforeCapture(
+            @NonNull List<CaptureRequest> captureRequests, boolean isStillCapture) {
+        if (!mShouldStopRepeatingBeforeStillCapture || !isStillCapture) {
+            return false;
+        }
+
+        for (CaptureRequest request : captureRequests) {
+            int aeMode = request.get(CaptureRequest.CONTROL_AE_MODE);
+            if (aeMode == CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+                    || aeMode == CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index ffa744e..f528b07c 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -32,18 +33,19 @@
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExposureState;
 import androidx.camera.core.TorchState;
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.internal.ImmutableZoomState;
+import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
@@ -78,6 +80,7 @@
     private CameraCharacteristicsCompat mCameraCharacteristics1;
     private ZoomControl mMockZoomControl;
     private TorchControl mMockTorchControl;
+    private ExposureControl mExposureControl;
     private Camera2CameraControlImpl mMockCameraControl;
 
     @Before
@@ -97,23 +100,26 @@
 
         mMockZoomControl = mock(ZoomControl.class);
         mMockTorchControl = mock(TorchControl.class);
+        mExposureControl = mock(ExposureControl.class);
         mMockCameraControl = mock(Camera2CameraControlImpl.class);
 
         when(mMockCameraControl.getZoomControl()).thenReturn(mMockZoomControl);
         when(mMockCameraControl.getTorchControl()).thenReturn(mMockTorchControl);
+        when(mMockCameraControl.getExposureControl()).thenReturn(mExposureControl);
     }
 
     @Test
     public void canCreateCameraInfo() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
         assertThat(cameraInfoInternal).isNotNull();
     }
 
     @Test
     public void cameraInfo_canReturnSensorOrientation() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
         assertThat(cameraInfoInternal.getSensorRotationDegrees()).isEqualTo(
                 CAMERA0_SENSOR_ORIENTATION);
     }
@@ -121,7 +127,7 @@
     @Test
     public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
 
         // Note: these numbers depend on the camera being a back-facing camera.
         assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -137,7 +143,7 @@
     @Test
     public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
 
         // Note: these numbers depend on the camera being a front-facing camera.
         assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -153,47 +159,130 @@
     @Test
     public void cameraInfo_canReturnLensFacing() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
         assertThat(cameraInfoInternal.getLensFacing()).isEqualTo(CAMERA0_LENS_FACING_ENUM);
     }
 
     @Test
     public void cameraInfo_canReturnHasFlashUnit_forBackCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
         assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA0_FLASH_INFO_BOOLEAN);
     }
 
     @Test
     public void cameraInfo_canReturnHasFlashUnit_forFrontCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
         assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA1_FLASH_INFO_BOOLEAN);
     }
 
     @Test
-    public void cameraInfo_canReturnTorchState() {
-        CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
-        when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.OFF));
-        assertThat(cameraInfoInternal.getTorchState().getValue()).isEqualTo(TorchState.OFF);
+    public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        assertThat(camera2CameraInfoImpl.getTorchState().getValue())
+                .isEqualTo(TorchControl.DEFAULT_TORCH_STATE);
+    }
+
+    @Test
+    public void cameraInfoWithCameraControl_canReturnTorchState() {
+        when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+        assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
+    }
+
+    @Test
+    public void torchStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+        // Calls getTorchState() to trigger RedirectableLiveData
+        LiveData<Integer> torchStateLiveData = camera2CameraInfoImpl.getTorchState();
+
+        when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        // TorchState LiveData instances are the same before and after the linkWithCameraControl.
+        assertThat(camera2CameraInfoImpl.getTorchState()).isSameInstanceAs(torchStateLiveData);
+        assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
     }
 
     // zoom related tests just ensure it uses ZoomControl to get the value
     // Full tests are performed at ZoomControlDeviceTest / ZoomControlTest.
     @Test
-    public void cameraInfo_getZoom_valueIsCorrect() {
-        CameraInfoInternal cameraInfo =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+    public void cameraInfoWithCameraControl_getZoom_valueIsCorrect() {
         ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
         when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
-        assertThat(mMockZoomControl.getZoomState().getValue()).isEqualTo(zoomState);
+
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        assertThat(camera2CameraInfoImpl.getZoomState().getValue()).isEqualTo(zoomState);
+    }
+
+    @Test
+    public void cameraInfoWithoutCameraControl_getDetaultZoomState() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        assertThat(camera2CameraInfoImpl.getZoomState().getValue())
+                .isEqualTo(ZoomControl.getDefaultZoomState(mCameraCharacteristics0));
+    }
+
+    @Test
+    public void zoomStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+        // Calls getZoomState() to trigger RedirectableLiveData
+        LiveData<ZoomState> zoomStateLiveData = camera2CameraInfoImpl.getZoomState();
+
+        ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
+        when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        // TorchState LiveData instances are the same before and after the linkWithCameraControl.
+        assertThat(camera2CameraInfoImpl.getZoomState()).isSameInstanceAs(zoomStateLiveData);
+        assertThat(camera2CameraInfoImpl.getZoomState().getValue()).isEqualTo(zoomState);
+    }
+
+    @Test
+    public void cameraInfoWithCameraControl_canReturnExposureState() {
+        ExposureState exposureState = new ExposureStateImpl(mCameraCharacteristics0, 2);
+        when(mExposureControl.getExposureState()).thenReturn(exposureState);
+
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        assertThat(camera2CameraInfoImpl.getExposureState()).isEqualTo(exposureState);
+    }
+
+    @Test
+    public void cameraInfoWithoutCameraControl_canReturnDefaultExposureState() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+        ExposureState defaultState =
+                ExposureControl.getDefaultExposureState(mCameraCharacteristics0);
+
+        assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationIndex())
+                .isEqualTo(defaultState.getExposureCompensationIndex());
+        assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationRange())
+                .isEqualTo(defaultState.getExposureCompensationRange());
+        assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationStep())
+                .isEqualTo(defaultState.getExposureCompensationStep());
+        assertThat(camera2CameraInfoImpl.getExposureState().isExposureCompensationSupported())
+                .isEqualTo(defaultState.isExposureCompensationSupported());
     }
 
     @Test
     public void cameraInfo_getImplementationType_legacy() {
         final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
-                mCameraCharacteristics0, mMockCameraControl);
+                mCameraCharacteristics0);
         assertThat(cameraInfo.getImplementationType()).isEqualTo(
                 CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
     }
@@ -201,43 +290,68 @@
     @Test
     public void cameraInfo_getImplementationType_noneLegacy() {
         final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
-                mCameraCharacteristics1, mMockCameraControl);
+                mCameraCharacteristics1);
         assertThat(cameraInfo.getImplementationType()).isEqualTo(
                 CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
     }
 
     @Test
     public void addSessionCameraCaptureCallback_isCalledToCameraControl() {
-        final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
-                mCameraCharacteristics1, mMockCameraControl);
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
 
         Executor executor = mock(Executor.class);
         CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
         cameraInfo.addSessionCaptureCallback(executor, callback);
 
-        ArgumentCaptor<Executor> executorCaptor = ArgumentCaptor.forClass(Executor.class);
-        ArgumentCaptor<CameraCaptureCallback> callbackCaptor =
-                ArgumentCaptor.forClass(CameraCaptureCallback.class);
-
-        verify(mMockCameraControl).addSessionCameraCaptureCallback(executorCaptor.capture(),
-                callbackCaptor.capture());
-        assertThat(executorCaptor.getValue()).isSameInstanceAs(executor);
-        assertThat(callbackCaptor.getValue()).isSameInstanceAs(callback);
+        verify(mMockCameraControl).addSessionCameraCaptureCallback(executor, callback);
     }
 
     @Test
     public void removeSessionCameraCaptureCallback_isCalledToCameraControl() {
-        final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
-                mCameraCharacteristics1, mMockCameraControl);
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
 
         CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
         cameraInfo.removeSessionCaptureCallback(callback);
 
-        ArgumentCaptor<CameraCaptureCallback> callbackCaptor =
-                ArgumentCaptor.forClass(CameraCaptureCallback.class);
+        verify(mMockCameraControl).removeSessionCameraCaptureCallback(callback);
+    }
 
-        verify(mMockCameraControl).removeSessionCameraCaptureCallback(callbackCaptor.capture());
-        assertThat(callbackCaptor.getValue()).isSameInstanceAs(callback);
+    @Test
+    public void addSessionCameraCaptureCallbackWithoutCameraControl_attachedToCameraControlLater() {
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        Executor executor = mock(Executor.class);
+        CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
+        cameraInfo.addSessionCaptureCallback(executor, callback);
+
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
+
+        verify(mMockCameraControl).addSessionCameraCaptureCallback(executor, callback);
+    }
+
+    @Test
+    public void removeSessionCameraCaptureCallbackWithoutCameraControl_callbackIsRemoved() {
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        // Add two callbacks
+        Executor executor1 = mock(Executor.class);
+        CameraCaptureCallback callback1 = mock(CameraCaptureCallback.class);
+        Executor executor2 = mock(Executor.class);
+        CameraCaptureCallback callback2 = mock(CameraCaptureCallback.class);
+        cameraInfo.addSessionCaptureCallback(executor1, callback1);
+        cameraInfo.addSessionCaptureCallback(executor2, callback2);
+
+        // Remove first callback.
+        cameraInfo.removeSessionCaptureCallback(callback1);
+
+        // Only second callback will be added to camera control.
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
+        verify(mMockCameraControl, never()).addSessionCameraCaptureCallback(executor1, callback1);
+        verify(mMockCameraControl).addSessionCameraCaptureCallback(executor2, callback2);
     }
 
     private void initCameras() {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index b248170..85d73124c 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -372,7 +372,9 @@
         useCases.add(preview);
 
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(LEGACY_CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         // A legacy level camera device can't support JPEG (ImageCapture) + PRIV (VideoCapture) +
         // PRIV (Preview) combination. An IllegalArgumentException will be thrown when trying to
@@ -399,7 +401,9 @@
         useCases.add(preview);
 
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(LIMITED_CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 mSurfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, Collections.emptyList(),
@@ -570,9 +574,7 @@
         @CameraSelector.LensFacing int lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
                 lensFacing);
         mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
-                new Camera2CameraInfoImpl(cameraId,
-                        getCameraCharacteristicsCompat(cameraId),
-                        mock(Camera2CameraControlImpl.class))));
+                new Camera2CameraInfoImpl(cameraId, getCameraCharacteristicsCompat(cameraId))));
     }
 
     private void initCameraX() {
@@ -592,11 +594,11 @@
 
         // Create the DeviceSurfaceManager for Camera2
         CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
-                (context, cameraManager) -> {
+                (context, cameraManager, availableCameraIds) -> {
                     try {
                         return new Camera2DeviceSurfaceManager(mContext,
                                 mMockCamcorderProfileHelper,
-                                (CameraManagerCompat) cameraManager);
+                                (CameraManagerCompat) cameraManager, availableCameraIds);
                     } catch (CameraUnavailableException e) {
                         throw new InitializationException(e);
                     }
@@ -608,7 +610,7 @@
 
         CameraXConfig.Builder appConfigBuilder =
                 new CameraXConfig.Builder()
-                        .setCameraFactoryProvider((ignored0, ignored1) -> mCameraFactory)
+                        .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> mCameraFactory)
                         .setDeviceSurfaceManagerProvider(surfaceManagerProvider)
                         .setUseCaseConfigFactoryProvider(factoryProvider);
 
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
index 1d588c5..a8ddb0e 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
@@ -117,4 +117,28 @@
         burstCaptureCallback.onCaptureStarted(mSession, mRequest0, 0, 0);
         // No listener called.
     }
+
+    @Test
+    public void captureSequenceCallback_calledWhenSequenceCompleted() {
+        CameraBurstCaptureCallback burstCaptureCallback = new CameraBurstCaptureCallback();
+        CameraBurstCaptureCallback.CaptureSequenceCallback sequenceCallback = mock(
+                CameraBurstCaptureCallback.CaptureSequenceCallback.class);
+        burstCaptureCallback.setCaptureSequenceCallback(sequenceCallback);
+
+        burstCaptureCallback.onCaptureSequenceCompleted(mSession, 0, 0);
+
+        verify(sequenceCallback).onCaptureSequenceCompletedOrAborted(mSession, 0, false);
+    }
+
+    @Test
+    public void captureSequenceCallback_calledWhenSequenceAborted() {
+        CameraBurstCaptureCallback burstCaptureCallback = new CameraBurstCaptureCallback();
+        CameraBurstCaptureCallback.CaptureSequenceCallback sequenceCallback = mock(
+                CameraBurstCaptureCallback.CaptureSequenceCallback.class);
+        burstCaptureCallback.setCaptureSequenceCallback(sequenceCallback);
+
+        burstCaptureCallback.onCaptureSequenceAborted(mSession, 0);
+
+        verify(sequenceCallback).onCaptureSequenceCompletedOrAborted(mSession, 0, true);
+    }
 }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java
new file mode 100644
index 0000000..de4b238
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.camera.camera2.interop.Camera2CameraFilter;
+import androidx.camera.camera2.interop.Camera2CameraInfo;
+import androidx.camera.core.CameraFilter;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.impl.CameraThreadConfig;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class CameraSelectionOptimizerTest {
+    private Camera2CameraFactory mCamera2CameraFactory;
+
+    @Before
+    public void setUp() throws Exception {
+        mCamera2CameraFactory =
+                spy(new Camera2CameraFactory(ApplicationProvider.getApplicationContext(),
+                        CameraThreadConfig.create(CameraXExecutors.mainThreadExecutor(),
+                                new Handler()),
+                        null));
+    }
+
+    void setupNormalCameras() throws Exception {
+        initCharacterisic("0", CameraCharacteristics.LENS_FACING_BACK, 3.52f);
+        initCharacterisic("1", CameraCharacteristics.LENS_FACING_FRONT, 3.52f);
+        initCharacterisic("2", CameraCharacteristics.LENS_FACING_BACK, 2.7f);
+        initCharacterisic("3", CameraCharacteristics.LENS_FACING_BACK, 10.0f);
+    }
+
+    void setupAbnormalCameras() throws Exception {
+        // "0" is front
+        initCharacterisic("0", CameraCharacteristics.LENS_FACING_FRONT, 3.52f);
+        // "1" is back
+        initCharacterisic("1", CameraCharacteristics.LENS_FACING_BACK, 3.52f);
+        initCharacterisic("2", CameraCharacteristics.LENS_FACING_BACK, 2.7f);
+        initCharacterisic("3", CameraCharacteristics.LENS_FACING_BACK, 10.0f);
+    }
+
+    @Test
+    public void availableCamerasSelectorNull_returnAllCameras() throws Exception {
+        setupNormalCameras();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        null);
+
+        assertThat(cameraIds).containsExactly("0", "1", "2", "3");
+    }
+
+    @Test
+    public void requireLensFacingBack() throws Exception {
+        setupNormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        assertThat(cameraIds).containsExactly("0", "2", "3");
+        verify(mCamera2CameraFactory, never()).getCameraInfo("1");
+    }
+
+    @Test
+    public void requireLensFacingFront() throws Exception {
+        setupNormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        assertThat(cameraIds).containsExactly("1");
+        // only camera "0" 's getCameraCharacteristics can be avoided.
+        verify(mCamera2CameraFactory, never()).getCameraInfo("0");
+    }
+
+    @Test
+    public void requireLensFacingBack_andSelectWidestAngle() throws Exception {
+        setupNormalCameras();
+
+        CameraFilter widestAngleFilter = Camera2CameraFilter.createCameraFilter(
+                cameraInfoList -> {
+                    float minFocalLength = 10000;
+                    Camera2CameraInfo minFocalCameraInfo = null;
+                    for (Camera2CameraInfo camera2CameraInfo : cameraInfoList) {
+                        float focalLength = camera2CameraInfo.getCameraCharacteristic(
+                                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)[0];
+                        if (focalLength < minFocalLength) {
+                            minFocalLength = focalLength;
+                            minFocalCameraInfo = camera2CameraInfo;
+                        }
+                    }
+                    return Arrays.asList(minFocalCameraInfo);
+                });
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+                        .addCameraFilter(widestAngleFilter)
+                        .build();
+
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        assertThat(cameraIds).containsExactly("2");
+        // only camera "1" 's getCameraCharacteristics can be avoided.
+        verify(mCamera2CameraFactory, never()).getCameraInfo("1");
+
+    }
+
+    @Test
+    public void abnormalCameraSetup_requireLensFacingBack() throws Exception {
+        setupAbnormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        // even though heuristic failed, it still works as expected.
+        assertThat(cameraIds).containsExactly("1", "2", "3");
+    }
+
+    @Test
+    public void abnormalCameraSetup_requireLensFacingFront() throws Exception {
+        setupAbnormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        // even though heuristic failed, it still works as expected.
+        assertThat(cameraIds).containsExactly("0");
+    }
+
+    private void initCharacterisic(String cameraId, int lensFacing, float focalLength) {
+        CameraCharacteristics characteristics =
+                ShadowCameraCharacteristics.newCameraCharacteristics();
+
+        ShadowCameraCharacteristics shadowCharacteristics = Shadow.extract(characteristics);
+
+        shadowCharacteristics.set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL,
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL);
+
+        // Add a lens facing to the camera
+        shadowCharacteristics.set(CameraCharacteristics.LENS_FACING, lensFacing);
+
+        shadowCharacteristics.set(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS,
+                new float[]{focalLength});
+
+        // Add the camera to the camera service
+        ((ShadowCameraManager)
+                Shadow.extract(
+                        ApplicationProvider.getApplicationContext()
+                                .getSystemService(Context.CAMERA_SERVICE)))
+                .addCamera(cameraId, characteristics);
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
index 129f334..e530f60 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
@@ -195,8 +195,7 @@
         cameraFactory.insertCamera(lensFacingEnum, BACK_CAMERA_ID,
                 () -> new FakeCamera(BACK_CAMERA_ID, null,
                         new Camera2CameraInfoImpl(BACK_CAMERA_ID,
-                                getCameraCharacteristicsCompat(BACK_CAMERA_ID),
-                                mock(Camera2CameraControlImpl.class))));
+                                getCameraCharacteristicsCompat(BACK_CAMERA_ID))));
 
         initCameraX(cameraFactory);
     }
@@ -204,7 +203,7 @@
     private void initCameraX(final FakeCameraFactory cameraFactory) {
         CameraXConfig cameraXConfig = CameraXConfig.Builder.fromConfig(
                 Camera2Config.defaultConfig())
-                .setCameraFactoryProvider((ignored0, ignored1) -> cameraFactory)
+                .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> cameraFactory)
                 .build();
         CameraX.initialize(mContext, cameraXConfig);
     }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
index c4bf842..5448c5d 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
@@ -495,7 +495,9 @@
         List<UseCase> useCases = new ArrayList<>();
         useCases.add(fakeUseCase);
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -605,7 +607,9 @@
         useCases.add(imageCapture);
         useCases.add(imageAnalysis);
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -653,7 +657,9 @@
         List<UseCase> useCases = new ArrayList<>();
         useCases.add(preview);
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -743,7 +749,9 @@
         useCases.add(videoCapture);
         useCases.add(preview);
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -775,7 +783,9 @@
         useCases.add(preview);
 
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -818,7 +828,9 @@
         useCases.add(preview);
         useCases.add(imageAnalysis);
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -853,7 +865,9 @@
         useCases.add(preview);
         useCases.add(imageAnalysis);
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -936,7 +950,9 @@
         useCases.add(videoCapture);
         useCases.add(preview);
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -1117,7 +1133,9 @@
         useCases.add(imageCapture);
 
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
-                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+                Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+                        mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+                        useCases,
                         mUseCaseConfigFactory);
         Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
                 supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -2172,8 +2190,7 @@
 
         mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
                 new Camera2CameraInfoImpl(cameraId,
-                        mCameraManagerCompat.getCameraCharacteristicsCompat(cameraId),
-                        mock(Camera2CameraControlImpl.class))));
+                        mCameraManagerCompat.getCameraCharacteristicsCompat(cameraId))));
 
         initCameraX();
     }
@@ -2181,7 +2198,7 @@
     private void initCameraX() {
         CameraXConfig cameraXConfig = CameraXConfig.Builder.fromConfig(
                 Camera2Config.defaultConfig())
-                .setCameraFactoryProvider((ignored0, ignored1) -> mCameraFactory)
+                .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> mCameraFactory)
                 .build();
         CameraX.initialize(mContext, cameraXConfig);
         CameraX cameraX;
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java
new file mode 100644
index 0000000..6f52773
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.workaround;
+
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(ParameterizedRobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class StillCaptureFlowTest {
+    @ParameterizedRobolectricTestRunner.Parameters
+    public static Collection<Object[]> data() {
+        final List<Object[]> data = new ArrayList<>();
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON, true, false});
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_AUTO_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_ALWAYS_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_AUTO_FLASH, false, false});
+
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON, true, false});
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_AUTO_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_ALWAYS_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_AUTO_FLASH, false, false});
+
+        data.add(new Object[]{"Google", "Pixel 2", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+        data.add(new Object[]{"Moto", "G3", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+        data.add(new Object[]{"Samsung", "SM-A722", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+
+        return data;
+    }
+
+    private final String mBrand;
+    private final String mModel;
+    private final int mAeMode;
+    private final boolean mIsStillCapture;
+    private final boolean mExpectedShouldStopRepeating;
+    public StillCaptureFlowTest(
+            String brand,
+            String model,
+            int aeMode,
+            boolean isStillCapture,
+            boolean expectedShouldStopRepeating) {
+        mBrand = brand;
+        mModel = model;
+        mAeMode = aeMode;
+        mIsStillCapture = isStillCapture;
+        mExpectedShouldStopRepeating = expectedShouldStopRepeating;
+    }
+
+    @Test
+    public void shouldStopRepeating() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", mBrand);
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", mModel);
+
+        StillCaptureFlow stillCaptureFlow = new StillCaptureFlow();
+        CaptureRequest captureRequest = mock(CaptureRequest.class);
+        when(captureRequest.get(CaptureRequest.CONTROL_AE_MODE)).thenReturn(mAeMode);
+
+        assertThat(stillCaptureFlow.shouldStopRepeatingBeforeCapture(
+                Arrays.asList(captureRequest), mIsStillCapture))
+                .isEqualTo(mExpectedShouldStopRepeating);
+    }
+}
diff --git a/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt b/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt
index e432572..45f7159b 100644
--- a/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt
+++ b/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt
@@ -69,12 +69,14 @@
   }
 
   public final class CameraXConfig {
+    method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraSelector? getAvailableCamerasSelector(androidx.camera.core.CameraSelector?);
     method @androidx.camera.core.ExperimentalLogging public int getMinimumLoggingLevel();
   }
 
   public static final class CameraXConfig.Builder {
     method public androidx.camera.core.CameraXConfig build();
     method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+    method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasSelector(androidx.camera.core.CameraSelector);
     method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
     method @androidx.camera.core.ExperimentalLogging public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
     method @androidx.camera.core.ExperimentalCustomizableThreads public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
@@ -88,6 +90,9 @@
     ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
   }
 
+  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasSelector {
+  }
+
   @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
   }
 
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index e432572..b10368c 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -69,12 +69,14 @@
   }
 
   public final class CameraXConfig {
+    method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraSelector? getAvailableCamerasLimiter(androidx.camera.core.CameraSelector?);
     method @androidx.camera.core.ExperimentalLogging public int getMinimumLoggingLevel();
   }
 
   public static final class CameraXConfig.Builder {
     method public androidx.camera.core.CameraXConfig build();
     method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+    method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasLimiter(androidx.camera.core.CameraSelector);
     method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
     method @androidx.camera.core.ExperimentalLogging public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
     method @androidx.camera.core.ExperimentalCustomizableThreads public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
@@ -88,6 +90,9 @@
     ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
   }
 
+  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasLimiter {
+  }
+
   @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
   }
 
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 0615e08..49d045a 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -15,7 +15,6 @@
  */
 
 import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
 import androidx.build.LibraryGroups
 import androidx.build.Publish
 
@@ -48,6 +47,7 @@
     testImplementation project(":camera:camera-testing"), {
         exclude group: "androidx.camera", module: "camera-core"
     }
+    testImplementation("androidx.exifinterface:exifinterface:1.0.0")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
index 779d45f..8157db1 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
@@ -165,9 +165,11 @@
     @Test
     public void init_withDifferentCameraXConfig() throws ExecutionException, InterruptedException {
         CameraFactory cameraFactory0 = new FakeCameraFactory();
-        CameraFactory.Provider cameraFactoryProvider0 = (ignored0, ignored1) -> cameraFactory0;
+        CameraFactory.Provider cameraFactoryProvider0 =
+                (ignored0, ignored1, ignored2) -> cameraFactory0;
         CameraFactory cameraFactory1 = new FakeCameraFactory();
-        CameraFactory.Provider cameraFactoryProvider1 = (ignored0, ignored1) -> cameraFactory1;
+        CameraFactory.Provider cameraFactoryProvider1 =
+                (ignored0, ignored1, ignored2) -> cameraFactory1;
 
         mConfigBuilder.setCameraFactoryProvider(cameraFactoryProvider0);
         CameraX.initialize(mContext, mConfigBuilder.build());
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java
index 34cba04..d6adbe0 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java
@@ -37,6 +37,7 @@
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.testing.fakes.FakeCameraInfoInternal;
 import androidx.camera.testing.fakes.FakeUseCase;
 import androidx.camera.testing.fakes.FakeUseCaseConfig;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -188,7 +189,10 @@
 
         TestUseCase testUseCase = new TestUseCase(useCaseConfig);
 
-        UseCaseConfig<?> mergedConfig = testUseCase.mergeConfigs(extendedConfig, defaultConfig);
+        FakeCameraInfoInternal cameraInfo = new FakeCameraInfoInternal();
+
+        UseCaseConfig<?> mergedConfig = testUseCase.mergeConfigs(cameraInfo, extendedConfig,
+                defaultConfig);
 
         assertThat(mergedConfig.getSurfaceOccupancyPriority()).isEqualTo(cameraDefaultPriority);
         assertThat(mergedConfig.getInputFormat()).isEqualTo(useCaseImageFormat);
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/utils/ExifOutputStreamTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/utils/ExifOutputStreamTest.kt
new file mode 100644
index 0000000..8b88aa5
--- /dev/null
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/utils/ExifOutputStreamTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils
+
+import android.graphics.Bitmap
+import android.os.Build
+import androidx.camera.core.impl.CameraCaptureMetaData
+import androidx.exifinterface.media.ExifInterface
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import java.io.File
+
+@LargeTest
+public class ExifOutputStreamTest {
+
+    @Test
+    public fun canSetExifOnCompressedBitmap() {
+        // Arrange.
+        val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
+        val exifData = ExifData.builderForDevice()
+            .setImageWidth(bitmap.width)
+            .setImageHeight(bitmap.height)
+            .setFlashState(CameraCaptureMetaData.FlashState.NONE)
+            .setExposureTimeNanos(0)
+            .build()
+
+        val fileWithExif = File.createTempFile("testWithExif", ".jpg")
+        val outputStreamWithExif = ExifOutputStream(fileWithExif.outputStream(), exifData)
+        fileWithExif.deleteOnExit()
+        val fileWithoutExif = File.createTempFile("testWithoutExif", ".jpg")
+        val outputStreamWithoutExif = fileWithoutExif.outputStream()
+        fileWithoutExif.deleteOnExit()
+
+        // Act.
+        bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStreamWithExif)
+        bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStreamWithoutExif)
+
+        // Verify with ExifInterface
+        val withExif = ExifInterface(fileWithExif.inputStream())
+        val withoutExif = ExifInterface(fileWithoutExif.inputStream())
+
+        // Assert.
+        // Model comes from default builder
+        assertThat(withExif.getAttribute(ExifInterface.TAG_MODEL)).isEqualTo(Build.MODEL)
+        assertThat(withoutExif.getAttribute(ExifInterface.TAG_MODEL)).isNull()
+
+        assertThat(withExif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).isEqualTo("100")
+        assertThat(withoutExif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).isEqualTo("100")
+
+        assertThat(withExif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).isEqualTo("100")
+        assertThat(withoutExif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).isEqualTo("100")
+
+        assertThat(withExif.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+            .isEqualTo(ExifInterface.FLAG_FLASH_NO_FLASH_FUNCTION)
+        assertThat(withoutExif.getAttribute(ExifInterface.TAG_FLASH)).isNull()
+
+        assertThat(withExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)?.toFloat()?.toInt())
+            .isEqualTo(0)
+        assertThat(withoutExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)).isNull()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
index 74dc16b..eb06e28 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
@@ -74,12 +74,7 @@
             executor = mThreadPoolExecutor;
         }
 
-        int cameraNumber = 0;
-        try {
-            cameraNumber = cameraFactory.getAvailableCameraIds().size();
-        } catch (CameraUnavailableException e) {
-            e.printStackTrace();
-        }
+        int cameraNumber = cameraFactory.getAvailableCameraIds().size();
         // According to the document of ThreadPoolExecutor, "If there are more than corePoolSize
         // but less than maximumPoolSize threads running, a new thread will be created only if
         // the queue is full."
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
index 13e5d31..e4d5954 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
@@ -32,7 +32,8 @@
 import java.util.List;
 
 /**
- * A set of requirements and priorities used to select a camera.
+ * A set of requirements and priorities used to select a camera or return a filtered set of
+ * cameras.
  */
 public final class CameraSelector {
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index cf53190..2d630a2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -31,6 +31,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
 import androidx.camera.core.impl.CameraFactory;
 import androidx.camera.core.impl.CameraInternal;
@@ -538,6 +539,7 @@
     /**
      * Initializes camera stack on the given thread and retry recursively until timeout.
      */
+    @UseExperimental(markerClass = ExperimentalAvailableCamerasLimiter.class)
     private void initAndRetryRecursively(
             @NonNull Executor cameraExecutor,
             long startMs,
@@ -562,8 +564,10 @@
                 CameraThreadConfig cameraThreadConfig = CameraThreadConfig.create(mCameraExecutor,
                         mSchedulerHandler);
 
+                CameraSelector availableCamerasLimiter =
+                        mCameraXConfig.getAvailableCamerasLimiter(null);
                 mCameraFactory = cameraFactoryProvider.newInstance(mAppContext,
-                        cameraThreadConfig);
+                        cameraThreadConfig, availableCamerasLimiter);
                 CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
                         mCameraXConfig.getDeviceSurfaceManagerProvider(null);
                 if (surfaceManagerProvider == null) {
@@ -572,7 +576,8 @@
                                     + "CameraDeviceSurfaceManager."));
                 }
                 mSurfaceManager = surfaceManagerProvider.newInstance(mAppContext,
-                        mCameraFactory.getCameraManager());
+                        mCameraFactory.getCameraManager(),
+                        mCameraFactory.getAvailableCameraIds());
 
                 UseCaseConfigFactory.Provider configFactoryProvider =
                         mCameraXConfig.getUseCaseConfigFactoryProvider(null);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java
index 7290fb4..4ac5e41 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java
@@ -101,6 +101,10 @@
             Option.create(
                     "camerax.core.appConfig.minimumLoggingLevel",
                     int.class);
+    static final Option<CameraSelector> OPTION_AVAILABLE_CAMERAS_LIMITER =
+            Option.create(
+                    "camerax.core.appConfig.availableCamerasLimiter",
+                    CameraSelector.class);
 
     // *********************************************************************************************
 
@@ -178,6 +182,16 @@
         return mConfig.retrieveOption(OPTION_MIN_LOGGING_LEVEL, Logger.DEFAULT_MIN_LOG_LEVEL);
     }
 
+    /**
+     * Returns the {@link CameraSelector} used to determine the available cameras.
+     */
+    @ExperimentalAvailableCamerasLimiter
+    @Nullable
+    public CameraSelector getAvailableCamerasLimiter(@Nullable CameraSelector valueIfMissing) {
+        return mConfig.retrieveOption(OPTION_AVAILABLE_CAMERAS_LIMITER, valueIfMissing);
+    }
+
+
     /** @hide */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
@@ -329,6 +343,32 @@
             return this;
         }
 
+        /**
+         * Sets a {@link CameraSelector} to determine the available cameras which defines
+         * which cameras can be used in the application.
+         *
+         * <p>Only cameras selected by this CameraSelector can be used in the applications. If
+         * the application binds the use cases with a CameraSelector that selects a unavailable
+         * camera, a {@link IllegalArgumentException} will be thrown.
+         *
+         * <p>This configuration can help CameraX optimize the latency of CameraX initialization.
+         * The tasks CameraX initialization performs include enumerating cameras, querying
+         * CameraCharacteristics and retrieving properties preparing for resolution determination.
+         * On some low end devices, these could take significant amount of time. Using the API
+         * can avoid the initialization of unnecessary cameras and speed up the time for camera
+         * start-up. For example, if the application uses only back cameras, it can set this
+         * configuration by CameraSelector.DEFAULT_BACK_CAMERA and then CameraX will avoid
+         * initializing front cameras to reduce the latency.
+         */
+        @ExperimentalAvailableCamerasLimiter
+        @NonNull
+        public Builder setAvailableCamerasLimiter(
+                @NonNull CameraSelector availableCameraSelector) {
+            getMutableConfig().insertOption(OPTION_AVAILABLE_CAMERAS_LIMITER,
+                    availableCameraSelector);
+            return this;
+        }
+
         @NonNull
         private MutableConfig getMutableConfig() {
             return mMutableConfig;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
new file mode 100644
index 0000000..99fc74f
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.experimental.Experimental;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Denotes that the annotated method uses an experimental API that configures CameraX to
+ * limit the available cameras applications can use in order to optimize the initialization
+ * latency.
+ *
+ * <p>Once the configuration is enabled, only cameras selected by this CameraSelector can be used
+ * in the applications. If the application binds the use cases with a CameraSelector that selects
+ * a unavailable camera, a {@link IllegalArgumentException} will be thrown.
+ *
+ * <p>CameraX initialization performs tasks including enumerating cameras, querying
+ * CameraCharacteristics and retrieving properties preparing for resolution determination. On
+ * some low end devices, these could take significant amount of time. Using the API can avoid the
+ * initialization of unnecessary cameras and speed up the time for camera start-up. For example,
+ * if the application uses only back cameras, it can set this configuration by
+ * CameraSelector.DEFAULT_BACK_CAMERA and then CameraX will avoid initializing front cameras to
+ * reduce the latency.
+ */
+@Retention(CLASS)
+@Experimental
+public @interface ExperimentalAvailableCamerasLimiter {
+}
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 a962f19..16e340b 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
@@ -80,6 +80,7 @@
 import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
 import androidx.camera.core.impl.CameraCaptureResult;
 import androidx.camera.core.impl.CameraCaptureResult.EmptyCameraCaptureResult;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CaptureBundle;
 import androidx.camera.core.impl.CaptureConfig;
@@ -463,7 +464,8 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
     @Override
-    UseCaseConfig<?> onMergeConfig(@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+    UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
         // Update the input format base on the other options set (mainly whether processing
         // is done)
         Integer bufferFormat = builder.getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
index b8a2b6b..f5feed0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
@@ -19,6 +19,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
 
 /** Metadata for an image. */
 public interface ImageInfo {
@@ -64,4 +65,12 @@
      */
     // TODO(b/122806727) Need to correctly set EXIF in JPEG images
     int getRotationDegrees();
+
+    /**
+     * Adds any stored EXIF information in this ImageInfo into the provided ExifData builder.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    void populateExifData(@NonNull ExifData.Builder exifBuilder);
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java
index 1e7a45d..010bc49 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
 
 import com.google.auto.value.AutoValue;
 
@@ -37,4 +38,10 @@
 
     @Override
     public abstract int getRotationDegrees();
+
+    @Override
+    public void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+        // Only have access to orientation information.
+        exifBuilder.setOrientationDegrees(getRotationDegrees());
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index e01c28d..e5f4e41 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -60,6 +60,7 @@
 import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.CaptureProcessor;
@@ -465,7 +466,8 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
     @Override
-    UseCaseConfig<?> onMergeConfig(@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+    UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
         if (builder.getMutableConfig().retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, null)
                 != null) {
             builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index ec92f42..ca32637 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -30,6 +30,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.camera.core.impl.CameraControlInternal;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.Config.Option;
@@ -164,6 +165,7 @@
     /**
      * Create a merged {@link UseCaseConfig} from the UseCase, camera, and an extended config.
      *
+     * @param cameraInfo          info about the camera which may be used to resolve conflicts.
      * @param extendedConfig      configs that take priority over the UseCase's default config
      * @param cameraDefaultConfig configs that have lower priority than the UseCase's default.
      *                            This Config comes from the camera implementation.
@@ -175,6 +177,7 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
     public UseCaseConfig<?> mergeConfigs(
+            @NonNull CameraInfoInternal cameraInfo,
             @Nullable UseCaseConfig<?> extendedConfig,
             @Nullable UseCaseConfig<?> cameraDefaultConfig) {
         MutableOptionsBundle mergedConfig;
@@ -199,8 +202,7 @@
 
         if (extendedConfig != null) {
             // If any options need special handling, this is the place to do it. For now we'll
-            // just copy
-            // over all options.
+            // just copy over all options.
             for (Option<?> opt : extendedConfig.listOptions()) {
                 @SuppressWarnings("unchecked") // Options/values are being copied directly
                         Option<Object> objectOpt = (Option<Object>) opt;
@@ -222,7 +224,7 @@
             mergedConfig.removeOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO);
         }
 
-        return onMergeConfig(getUseCaseConfigBuilder(mergedConfig));
+        return onMergeConfig(cameraInfo, getUseCaseConfigBuilder(mergedConfig));
     }
 
     /**
@@ -231,8 +233,9 @@
      * <p> This can be overridden by a UseCase which need to do additional verification of the
      * configs to make sure there are no conflicting options.
      *
-     * @param builder the builder containing the merged configs requiring addition conflict
-     *                resolution
+     * @param cameraInfo info about the camera which may be used to resolve conflicts.
+     * @param builder    the builder containing the merged configs requiring addition conflict
+     *                   resolution
      * @return the conflict resolved config
      * @throws IllegalArgumentException if there exists conflicts in the merged config that can
      * not be resolved
@@ -240,7 +243,8 @@
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
-    UseCaseConfig<?> onMergeConfig(@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+    UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
         return builder.getUseCaseConfig();
     }
 
@@ -263,7 +267,16 @@
             UseCaseConfigUtil.updateTargetRotationAndRelatedConfigs(builder, targetRotation);
             mUseCaseConfig = builder.getUseCaseConfig();
 
-            mCurrentConfig = mergeConfigs(mExtendedConfig, mCameraConfig);
+            // Only merge configs if currently attached to a camera. Otherwise, set the current
+            // config to the use case config and mergeConfig() will be called once the use case
+            // is attached to a camera.
+            CameraInternal camera = getCamera();
+            if (camera == null) {
+                mCurrentConfig = mUseCaseConfig;
+            } else {
+                mCurrentConfig = mergeConfigs(camera.getCameraInfoInternal(), mExtendedConfig,
+                        mCameraConfig);
+            }
 
             return true;
         }
@@ -531,7 +544,8 @@
 
         mExtendedConfig = extendedConfig;
         mCameraConfig = cameraConfig;
-        mCurrentConfig = mergeConfigs(mExtendedConfig, mCameraConfig);
+        mCurrentConfig = mergeConfigs(camera.getCameraInfoInternal(), mExtendedConfig,
+                mCameraConfig);
 
         EventCallback eventCallback = mCurrentConfig.getUseCaseEventCallback(null);
         if (eventCallback != null) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
index 12fe5ea..ba1a31e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
@@ -622,6 +622,7 @@
                 if (isCurrentCamera(cameraId)) {
                     // Only reset the pipeline when the bound camera is the same.
                     setupEncoder(cameraId, resolution);
+                    notifyReset();
                 }
             }
         });
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java
index abdc327..a808987 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java
@@ -22,6 +22,7 @@
 import androidx.camera.core.impl.CameraCaptureMetaData.AfState;
 import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
 import androidx.camera.core.impl.CameraCaptureMetaData.FlashState;
+import androidx.camera.core.impl.utils.ExifData;
 
 /**
  * The result of a single image capture.
@@ -60,6 +61,11 @@
     @NonNull
     TagBundle getTagBundle();
 
+    /** Populates the given Exif.Builder with attributes from this CameraCaptureResult. */
+    default void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+        exifBuilder.setFlashState(getFlashState());
+    }
+
     /** An implementation of CameraCaptureResult which always return default results. */
     final class EmptyCameraCaptureResult implements CameraCaptureResult {
 
@@ -106,7 +112,7 @@
         @Override
         @NonNull
         public TagBundle getTagBundle() {
-            return null;
+            return TagBundle.emptyBundle();
         }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index d0a633d..00df7bd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -25,6 +25,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Camera device manager to provide the guaranteed supported stream capabilities related info for
@@ -41,12 +42,13 @@
          *
          * @param context the android context
          * @param cameraManager the camera manager object used to query the camera information.
+         * @param availableCameraIds current available camera ids.
          * @return the factory instance
          * @throws InitializationException if it fails to create the factory
          */
         @NonNull
         CameraDeviceSurfaceManager newInstance(@NonNull Context context,
-                @Nullable Object cameraManager)
+                @Nullable Object cameraManager, @NonNull Set<String> availableCameraIds)
                 throws InitializationException;
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
index 6f35f16..1e87b19 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.InitializationException;
 
@@ -39,11 +40,14 @@
          *
          * @param context the android context
          * @param threadConfig the thread config to run the camera operations
+         * @param availableCamerasLimiter a CameraSelector used to specify which cameras will be
+         *                                 loaded and available to CameraX.
          * @return the factory instance
          * @throws InitializationException if it fails to create the factory.
          */
         @NonNull CameraFactory newInstance(@NonNull Context context,
-                @NonNull CameraThreadConfig threadConfig) throws InitializationException;
+                @NonNull CameraThreadConfig threadConfig,
+                @Nullable CameraSelector availableCamerasLimiter) throws InitializationException;
     }
 
     /**
@@ -63,11 +67,9 @@
      * Gets the ids of all available cameras.
      *
      * @return the list of available cameras
-     * @throws CameraUnavailableException if unable to access cameras, perhaps due
-     *                                    to insufficient permissions.
      */
     @NonNull
-    Set<String> getAvailableCameraIds() throws CameraUnavailableException;
+    Set<String> getAvailableCameraIds();
 
     /**
      * Gets the camera manager instance that is used to access the camera API.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index e751bc1..7dc5669 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -62,4 +62,8 @@
      * {@link #addSessionCaptureCallback(Executor, CameraCaptureCallback)}.
      */
     void removeSessionCaptureCallback(@NonNull CameraCaptureCallback callback);
+
+    /** Returns a list of quirks related to the camera. */
+    @NonNull
+    Quirks getCameraQuirks();
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
index 9c5226f..8422efc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
@@ -153,10 +153,6 @@
     @NonNull
     CameraInfoInternal getCameraInfoInternal();
 
-    /** Returns a list of quirks related to the camera. */
-    @NonNull
-    Quirks getCameraQuirks();
-
     ////////////////////////////////////////////////////////////////////////////////////////////////
     // Camera interface
     ////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
index 2246074..5e3871f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
@@ -39,6 +39,10 @@
     /**
      * Retrieves a {@link Quirk} instance given its type.
      *
+     * <p>Unlike {@link #contains(Class)}, a quirk can only be retrieved by the exact class. If a
+     * superclass or superinterface is provided, {@code null} will be returned, even if a quirk
+     * with the provided superclass or superinterface exists in this collection.
+     *
      * @param quirkClass The type of quirk to retrieve.
      * @return A {@link Quirk} instance of the provided type, or {@code null} if it isn't found.
      */
@@ -52,4 +56,23 @@
         }
         return null;
     }
+
+    /**
+     * Returns whether this collection of quirks contains a quirk with the provided type.
+     *
+     * <p>This checks whether the provided quirk type is the exact class, a superclass, or a
+     * superinterface of any of the contained quirks, and will return true in all cases.
+     * @param quirkClass The type of quirk to check for existence in this container.
+     * @return {@code true} if this container contains a quirk with the given type, {@code false}
+     * otherwise.
+     */
+    public boolean contains(@NonNull Class<? extends Quirk> quirkClass) {
+        for (Quirk quirk : mQuirks) {
+            if (quirkClass.isAssignableFrom(quirk.getClass())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataInputStream.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataInputStream.java
new file mode 100644
index 0000000..318761b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataInputStream.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+
+/**
+ * An input stream to parse EXIF data area, which can be written in either little or big endian
+ * order.
+ */
+// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}
+final class ByteOrderedDataInputStream extends InputStream implements DataInput {
+    private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
+    private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;
+
+    private final DataInputStream mDataInputStream;
+    private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final int mLength;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+            int mPosition;
+
+    ByteOrderedDataInputStream(InputStream in) throws IOException {
+        this(in, ByteOrder.BIG_ENDIAN);
+    }
+
+    ByteOrderedDataInputStream(InputStream in, ByteOrder byteOrder) throws IOException {
+        mDataInputStream = new DataInputStream(in);
+        mLength = mDataInputStream.available();
+        mPosition = 0;
+        // TODO (b/142218289): Need to handle case where input stream does not support mark
+        mDataInputStream.mark(mLength);
+        mByteOrder = byteOrder;
+    }
+
+    ByteOrderedDataInputStream(byte[] bytes) throws IOException {
+        this(new ByteArrayInputStream(bytes));
+    }
+
+    public void setByteOrder(ByteOrder byteOrder) {
+        mByteOrder = byteOrder;
+    }
+
+    public void seek(long byteCount) throws IOException {
+        if (mPosition > byteCount) {
+            mPosition = 0;
+            mDataInputStream.reset();
+            // TODO (b/142218289): Need to handle case where input stream does not support mark
+            mDataInputStream.mark(mLength);
+        } else {
+            byteCount -= mPosition;
+        }
+
+        if (skipBytes((int) byteCount) != (int) byteCount) {
+            throw new IOException("Couldn't seek up to the byteCount");
+        }
+    }
+
+    public int peek() {
+        return mPosition;
+    }
+
+    @Override
+    public int available() throws IOException {
+        return mDataInputStream.available();
+    }
+
+    @Override
+    public int read() throws IOException {
+        ++mPosition;
+        return mDataInputStream.read();
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        int bytesRead = mDataInputStream.read(b, off, len);
+        mPosition += bytesRead;
+        return bytesRead;
+    }
+
+    @Override
+    public int readUnsignedByte() throws IOException {
+        ++mPosition;
+        return mDataInputStream.readUnsignedByte();
+    }
+
+    @Override
+    public String readLine() {
+        throw new UnsupportedOperationException("readLine() not implemented.");
+    }
+
+    @Override
+    public boolean readBoolean() throws IOException {
+        ++mPosition;
+        return mDataInputStream.readBoolean();
+    }
+
+    @Override
+    public char readChar() throws IOException {
+        mPosition += 2;
+        return mDataInputStream.readChar();
+    }
+
+    @Override
+    public String readUTF() throws IOException {
+        mPosition += 2;
+        return mDataInputStream.readUTF();
+    }
+
+    @Override
+    public void readFully(byte[] buffer, int offset, int length) throws IOException {
+        mPosition += length;
+        if (mPosition > mLength) {
+            throw new EOFException();
+        }
+        if (mDataInputStream.read(buffer, offset, length) != length) {
+            throw new IOException("Couldn't read up to the length of buffer");
+        }
+    }
+
+    @Override
+    public void readFully(byte[] buffer) throws IOException {
+        mPosition += buffer.length;
+        if (mPosition > mLength) {
+            throw new EOFException();
+        }
+        if (mDataInputStream.read(buffer, 0, buffer.length) != buffer.length) {
+            throw new IOException("Couldn't read up to the length of buffer");
+        }
+    }
+
+    @Override
+    public byte readByte() throws IOException {
+        ++mPosition;
+        if (mPosition > mLength) {
+            throw new EOFException();
+        }
+        int ch = mDataInputStream.read();
+        if (ch < 0) {
+            throw new EOFException();
+        }
+        return (byte) ch;
+    }
+
+    @Override
+    public short readShort() throws IOException {
+        mPosition += 2;
+        if (mPosition > mLength) {
+            throw new EOFException();
+        }
+        int ch1 = mDataInputStream.read();
+        int ch2 = mDataInputStream.read();
+        if ((ch1 | ch2) < 0) {
+            throw new EOFException();
+        }
+        if (mByteOrder == LITTLE_ENDIAN) {
+            return (short) ((ch2 << 8) + ch1);
+        } else if (mByteOrder == BIG_ENDIAN) {
+            return (short) ((ch1 << 8) + ch2);
+        }
+        throw new IOException("Invalid byte order: " + mByteOrder);
+    }
+
+    @Override
+    public int readInt() throws IOException {
+        mPosition += 4;
+        if (mPosition > mLength) {
+            throw new EOFException();
+        }
+        int ch1 = mDataInputStream.read();
+        int ch2 = mDataInputStream.read();
+        int ch3 = mDataInputStream.read();
+        int ch4 = mDataInputStream.read();
+        if ((ch1 | ch2 | ch3 | ch4) < 0) {
+            throw new EOFException();
+        }
+        if (mByteOrder == LITTLE_ENDIAN) {
+            return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
+        } else if (mByteOrder == BIG_ENDIAN) {
+            return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
+        }
+        throw new IOException("Invalid byte order: " + mByteOrder);
+    }
+
+    @Override
+    public int skipBytes(int byteCount) throws IOException {
+        int totalSkip = Math.min(byteCount, mLength - mPosition);
+        int skipped = 0;
+        while (skipped < totalSkip) {
+            skipped += mDataInputStream.skipBytes(totalSkip - skipped);
+        }
+        mPosition += skipped;
+        return skipped;
+    }
+
+    @Override
+    public int readUnsignedShort() throws IOException {
+        mPosition += 2;
+        if (mPosition > mLength) {
+            throw new EOFException();
+        }
+        int ch1 = mDataInputStream.read();
+        int ch2 = mDataInputStream.read();
+        if ((ch1 | ch2) < 0) {
+            throw new EOFException();
+        }
+        if (mByteOrder == LITTLE_ENDIAN) {
+            return ((ch2 << 8) + ch1);
+        } else if (mByteOrder == BIG_ENDIAN) {
+            return ((ch1 << 8) + ch2);
+        }
+        throw new IOException("Invalid byte order: " + mByteOrder);
+    }
+
+    public long readUnsignedInt() throws IOException {
+        return readInt() & 0xffffffffL;
+    }
+
+    @Override
+    public long readLong() throws IOException {
+        mPosition += 8;
+        if (mPosition > mLength) {
+            throw new EOFException();
+        }
+        int ch1 = mDataInputStream.read();
+        int ch2 = mDataInputStream.read();
+        int ch3 = mDataInputStream.read();
+        int ch4 = mDataInputStream.read();
+        int ch5 = mDataInputStream.read();
+        int ch6 = mDataInputStream.read();
+        int ch7 = mDataInputStream.read();
+        int ch8 = mDataInputStream.read();
+        if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) {
+            throw new EOFException();
+        }
+        if (mByteOrder == LITTLE_ENDIAN) {
+            return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
+                    + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
+                    + ((long) ch2 << 8) + (long) ch1);
+        } else if (mByteOrder == BIG_ENDIAN) {
+            return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
+                    + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
+                    + ((long) ch7 << 8) + (long) ch8);
+        }
+        throw new IOException("Invalid byte order: " + mByteOrder);
+    }
+
+    @Override
+    public float readFloat() throws IOException {
+        return Float.intBitsToFloat(readInt());
+    }
+
+    @Override
+    public double readDouble() throws IOException {
+        return Double.longBitsToDouble(readLong());
+    }
+
+    @Override
+    public void mark(int readlimit) {
+        synchronized (mDataInputStream) {
+            mDataInputStream.mark(readlimit);
+        }
+    }
+
+    public int getLength() {
+        return mLength;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataOutputStream.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataOutputStream.java
new file mode 100644
index 0000000..5f830f8
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataOutputStream.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteOrder;
+
+/**
+ * An output stream to write EXIF data area, which can be written in either little or big endian
+ * order.
+ */
+// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}
+class ByteOrderedDataOutputStream extends FilterOutputStream {
+    final OutputStream mOutputStream;
+    private ByteOrder mByteOrder;
+
+    ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
+        super(out);
+        mOutputStream = out;
+        mByteOrder = byteOrder;
+    }
+
+    public void setByteOrder(ByteOrder byteOrder) {
+        mByteOrder = byteOrder;
+    }
+
+    @Override
+    public void write(byte[] bytes) throws IOException {
+        mOutputStream.write(bytes);
+    }
+
+    @Override
+    public void write(byte[] bytes, int offset, int length) throws IOException {
+        mOutputStream.write(bytes, offset, length);
+    }
+
+    public void writeByte(int val) throws IOException {
+        mOutputStream.write(val);
+    }
+
+    public void writeShort(short val) throws IOException {
+        if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
+            mOutputStream.write((val >>> 0) & 0xFF);
+            mOutputStream.write((val >>> 8) & 0xFF);
+        } else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
+            mOutputStream.write((val >>> 8) & 0xFF);
+            mOutputStream.write((val >>> 0) & 0xFF);
+        }
+    }
+
+    public void writeInt(int val) throws IOException {
+        if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
+            mOutputStream.write((val >>> 0) & 0xFF);
+            mOutputStream.write((val >>> 8) & 0xFF);
+            mOutputStream.write((val >>> 16) & 0xFF);
+            mOutputStream.write((val >>> 24) & 0xFF);
+        } else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
+            mOutputStream.write((val >>> 24) & 0xFF);
+            mOutputStream.write((val >>> 16) & 0xFF);
+            mOutputStream.write((val >>> 8) & 0xFF);
+            mOutputStream.write((val >>> 0) & 0xFF);
+        }
+    }
+
+    public void writeUnsignedShort(int val) throws IOException {
+        writeShort((short) val);
+    }
+
+    public void writeUnsignedInt(long val) throws IOException {
+        writeInt((int) val);
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifAttribute.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifAttribute.java
new file mode 100644
index 0000000..02a1d39
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifAttribute.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * A class for indicating EXIF attribute.
+ *
+ * This class was pulled from the {@link androidx.exifinterface.media.ExifInterface} class.
+ */
+final class ExifAttribute {
+    private static final String TAG = "ExifAttribute";
+    public static final long BYTES_OFFSET_UNKNOWN = -1;
+
+    // See JPEG File Interchange Format Version 1.02.
+    // The following values are defined for handling JPEG streams. In this implementation, we are
+    // not only getting information from EXIF but also from some JPEG special segments such as
+    // MARKER_COM for user comment and MARKER_SOFx for image width and height.
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static final Charset ASCII = StandardCharsets.US_ASCII;
+
+    // Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".)
+    static final int IFD_FORMAT_BYTE = 1;
+    static final int IFD_FORMAT_STRING = 2;
+    static final int IFD_FORMAT_USHORT = 3;
+    static final int IFD_FORMAT_ULONG = 4;
+    static final int IFD_FORMAT_URATIONAL = 5;
+    static final int IFD_FORMAT_SBYTE = 6;
+    static final int IFD_FORMAT_UNDEFINED = 7;
+    static final int IFD_FORMAT_SSHORT = 8;
+    static final int IFD_FORMAT_SLONG = 9;
+    static final int IFD_FORMAT_SRATIONAL = 10;
+    static final int IFD_FORMAT_SINGLE = 11;
+    static final int IFD_FORMAT_DOUBLE = 12;
+    // Names for the data formats for debugging purpose.
+    static final String[] IFD_FORMAT_NAMES = new String[] {
+            "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
+            "SLONG", "SRATIONAL", "SINGLE", "DOUBLE", "IFD"
+    };
+    // Sizes of the components of each IFD value format
+    static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
+            0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 1
+    };
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static final byte[] EXIF_ASCII_PREFIX = new byte[] {
+            0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
+    };
+
+    public final int format;
+    public final int numberOfComponents;
+    public final long bytesOffset;
+    public final byte[] bytes;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ExifAttribute(int format, int numberOfComponents, byte[] bytes) {
+        this(format, numberOfComponents, BYTES_OFFSET_UNKNOWN, bytes);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ExifAttribute(int format, int numberOfComponents, long bytesOffset, byte[] bytes) {
+        this.format = format;
+        this.numberOfComponents = numberOfComponents;
+        this.bytesOffset = bytesOffset;
+        this.bytes = bytes;
+    }
+
+    @NonNull
+    public static ExifAttribute createUShort(@NonNull int[] values, @NonNull ByteOrder byteOrder) {
+        final ByteBuffer buffer = ByteBuffer.wrap(
+                new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]);
+        buffer.order(byteOrder);
+        for (int value : values) {
+            buffer.putShort((short) value);
+        }
+        return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array());
+    }
+
+    @NonNull
+    public static ExifAttribute createUShort(int value, @NonNull ByteOrder byteOrder) {
+        return createUShort(new int[] {value}, byteOrder);
+    }
+
+    @NonNull
+    public static ExifAttribute createULong(@NonNull long[] values, @NonNull ByteOrder byteOrder) {
+        final ByteBuffer buffer = ByteBuffer.wrap(
+                new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]);
+        buffer.order(byteOrder);
+        for (long value : values) {
+            buffer.putInt((int) value);
+        }
+        return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array());
+    }
+
+    @NonNull
+    public static ExifAttribute createULong(long value, @NonNull ByteOrder byteOrder) {
+        return createULong(new long[] {value}, byteOrder);
+    }
+
+    @NonNull
+    public static ExifAttribute createSLong(@NonNull int[] values, @NonNull ByteOrder byteOrder) {
+        final ByteBuffer buffer = ByteBuffer.wrap(
+                new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]);
+        buffer.order(byteOrder);
+        for (int value : values) {
+            buffer.putInt(value);
+        }
+        return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array());
+    }
+
+    @NonNull
+    public static ExifAttribute createSLong(int value, @NonNull ByteOrder byteOrder) {
+        return createSLong(new int[] {value}, byteOrder);
+    }
+
+    @NonNull
+    public static ExifAttribute createByte(@NonNull String value) {
+        // Exception for GPSAltitudeRef tag
+        if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') {
+            final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') };
+            return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes);
+        }
+        final byte[] ascii = value.getBytes(ASCII);
+        return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii);
+    }
+
+    @NonNull
+    public static ExifAttribute createString(@NonNull String value) {
+        final byte[] ascii = (value + '\0').getBytes(ASCII);
+        return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii);
+    }
+
+    @NonNull
+    public static ExifAttribute createURational(@NonNull LongRational[] values,
+            @NonNull ByteOrder byteOrder) {
+        final ByteBuffer buffer = ByteBuffer.wrap(
+                new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]);
+        buffer.order(byteOrder);
+        for (LongRational value : values) {
+            buffer.putInt((int) value.getNumerator());
+            buffer.putInt((int) value.getDenominator());
+        }
+        return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array());
+    }
+
+    @NonNull
+    public static ExifAttribute createURational(@NonNull LongRational value,
+            @NonNull ByteOrder byteOrder) {
+        return createURational(new LongRational[] {value}, byteOrder);
+    }
+
+    @NonNull
+    public static ExifAttribute createSRational(@NonNull LongRational[] values,
+            @NonNull ByteOrder byteOrder) {
+        final ByteBuffer buffer = ByteBuffer.wrap(
+                new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]);
+        buffer.order(byteOrder);
+        for (LongRational value : values) {
+            buffer.putInt((int) value.getNumerator());
+            buffer.putInt((int) value.getDenominator());
+        }
+        return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array());
+    }
+
+    @NonNull
+    public static ExifAttribute createSRational(@NonNull LongRational value,
+            @NonNull ByteOrder byteOrder) {
+        return createSRational(new LongRational[] {value}, byteOrder);
+    }
+
+    @NonNull
+    public static ExifAttribute createDouble(@NonNull double[] values,
+            @NonNull ByteOrder byteOrder) {
+        final ByteBuffer buffer = ByteBuffer.wrap(
+                new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]);
+        buffer.order(byteOrder);
+        for (double value : values) {
+            buffer.putDouble(value);
+        }
+        return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array());
+    }
+
+    @NonNull
+    public static ExifAttribute createDouble(double value, @NonNull ByteOrder byteOrder) {
+        return createDouble(new double[] {value}, byteOrder);
+    }
+
+    @Override
+    public String toString() {
+        return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")";
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    Object getValue(ByteOrder byteOrder) {
+        ByteOrderedDataInputStream inputStream = null;
+        try {
+            inputStream = new ByteOrderedDataInputStream(bytes);
+            inputStream.setByteOrder(byteOrder);
+            switch (format) {
+                case IFD_FORMAT_BYTE:
+                case IFD_FORMAT_SBYTE: {
+                    // Exception for GPSAltitudeRef tag
+                    if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) {
+                        return new String(new char[] { (char) (bytes[0] + '0') });
+                    }
+                    return new String(bytes, ASCII);
+                }
+                case IFD_FORMAT_UNDEFINED:
+                case IFD_FORMAT_STRING: {
+                    int index = 0;
+                    if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
+                        boolean same = true;
+                        for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
+                            if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
+                                same = false;
+                                break;
+                            }
+                        }
+                        if (same) {
+                            index = EXIF_ASCII_PREFIX.length;
+                        }
+                    }
+
+                    StringBuilder stringBuilder = new StringBuilder();
+                    while (index < numberOfComponents) {
+                        int ch = bytes[index];
+                        if (ch == 0) {
+                            break;
+                        }
+                        if (ch >= 32) {
+                            stringBuilder.append((char) ch);
+                        } else {
+                            stringBuilder.append('?');
+                        }
+                        ++index;
+                    }
+                    return stringBuilder.toString();
+                }
+                case IFD_FORMAT_USHORT: {
+                    final int[] values = new int[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        values[i] = inputStream.readUnsignedShort();
+                    }
+                    return values;
+                }
+                case IFD_FORMAT_ULONG: {
+                    final long[] values = new long[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        values[i] = inputStream.readUnsignedInt();
+                    }
+                    return values;
+                }
+                case IFD_FORMAT_URATIONAL: {
+                    final LongRational[] values = new LongRational[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        final long numerator = inputStream.readUnsignedInt();
+                        final long denominator = inputStream.readUnsignedInt();
+                        values[i] = new LongRational(numerator, denominator);
+                    }
+                    return values;
+                }
+                case IFD_FORMAT_SSHORT: {
+                    final int[] values = new int[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        values[i] = inputStream.readShort();
+                    }
+                    return values;
+                }
+                case IFD_FORMAT_SLONG: {
+                    final int[] values = new int[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        values[i] = inputStream.readInt();
+                    }
+                    return values;
+                }
+                case IFD_FORMAT_SRATIONAL: {
+                    final LongRational[] values = new LongRational[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        final long numerator = inputStream.readInt();
+                        final long denominator = inputStream.readInt();
+                        values[i] = new LongRational(numerator, denominator);
+                    }
+                    return values;
+                }
+                case IFD_FORMAT_SINGLE: {
+                    final double[] values = new double[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        values[i] = inputStream.readFloat();
+                    }
+                    return values;
+                }
+                case IFD_FORMAT_DOUBLE: {
+                    final double[] values = new double[numberOfComponents];
+                    for (int i = 0; i < numberOfComponents; ++i) {
+                        values[i] = inputStream.readDouble();
+                    }
+                    return values;
+                }
+                default:
+                    return null;
+            }
+        } catch (IOException e) {
+            Logger.w(TAG, "IOException occurred during reading a value", e);
+            return null;
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    Logger.e(TAG, "IOException occurred while closing InputStream", e);
+                }
+            }
+        }
+    }
+
+    public double getDoubleValue(@NonNull ByteOrder byteOrder) {
+        Object value = getValue(byteOrder);
+        if (value == null) {
+            throw new NumberFormatException("NULL can't be converted to a double value");
+        }
+        if (value instanceof String) {
+            return Double.parseDouble((String) value);
+        }
+        if (value instanceof long[]) {
+            long[] array = (long[]) value;
+            if (array.length == 1) {
+                return array[0];
+            }
+            throw new NumberFormatException("There are more than one component");
+        }
+        if (value instanceof int[]) {
+            int[] array = (int[]) value;
+            if (array.length == 1) {
+                return array[0];
+            }
+            throw new NumberFormatException("There are more than one component");
+        }
+        if (value instanceof double[]) {
+            double[] array = (double[]) value;
+            if (array.length == 1) {
+                return array[0];
+            }
+            throw new NumberFormatException("There are more than one component");
+        }
+        if (value instanceof LongRational[]) {
+            LongRational[] array = (LongRational[]) value;
+            if (array.length == 1) {
+                return array[0].toDouble();
+            }
+            throw new NumberFormatException("There are more than one component");
+        }
+        throw new NumberFormatException("Couldn't find a double value");
+    }
+
+    public int getIntValue(@NonNull ByteOrder byteOrder) {
+        Object value = getValue(byteOrder);
+        if (value == null) {
+            throw new NumberFormatException("NULL can't be converted to a integer value");
+        }
+        if (value instanceof String) {
+            return Integer.parseInt((String) value);
+        }
+        if (value instanceof long[]) {
+            long[] array = (long[]) value;
+            if (array.length == 1) {
+                return (int) array[0];
+            }
+            throw new NumberFormatException("There are more than one component");
+        }
+        if (value instanceof int[]) {
+            int[] array = (int[]) value;
+            if (array.length == 1) {
+                return array[0];
+            }
+            throw new NumberFormatException("There are more than one component");
+        }
+        throw new NumberFormatException("Couldn't find a integer value");
+    }
+
+    @Nullable
+    public String getStringValue(@NonNull ByteOrder byteOrder) {
+        Object value = getValue(byteOrder);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof String) {
+            return (String) value;
+        }
+
+        final StringBuilder stringBuilder = new StringBuilder();
+        if (value instanceof long[]) {
+            long[] array = (long[]) value;
+            for (int i = 0; i < array.length; ++i) {
+                stringBuilder.append(array[i]);
+                if (i + 1 != array.length) {
+                    stringBuilder.append(",");
+                }
+            }
+            return stringBuilder.toString();
+        }
+        if (value instanceof int[]) {
+            int[] array = (int[]) value;
+            for (int i = 0; i < array.length; ++i) {
+                stringBuilder.append(array[i]);
+                if (i + 1 != array.length) {
+                    stringBuilder.append(",");
+                }
+            }
+            return stringBuilder.toString();
+        }
+        if (value instanceof double[]) {
+            double[] array = (double[]) value;
+            for (int i = 0; i < array.length; ++i) {
+                stringBuilder.append(array[i]);
+                if (i + 1 != array.length) {
+                    stringBuilder.append(",");
+                }
+            }
+            return stringBuilder.toString();
+        }
+        if (value instanceof LongRational[]) {
+            LongRational[] array = (LongRational[]) value;
+            for (int i = 0; i < array.length; ++i) {
+                stringBuilder.append(array[i].getNumerator());
+                stringBuilder.append('/');
+                stringBuilder.append(array[i].getDenominator());
+                if (i + 1 != array.length) {
+                    stringBuilder.append(",");
+                }
+            }
+            return stringBuilder.toString();
+        }
+        return null;
+    }
+
+    public int size() {
+        return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
new file mode 100644
index 0000000..ea5d08a
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
@@ -0,0 +1,967 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_BYTE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_DOUBLE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SLONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SRATIONAL;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_STRING;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_ULONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_UNDEFINED;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_URATIONAL;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_USHORT;
+import static androidx.exifinterface.media.ExifInterface.CONTRAST_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.EXPOSURE_PROGRAM_NOT_DEFINED;
+import static androidx.exifinterface.media.ExifInterface.FILE_SOURCE_DSC;
+import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_FIRED;
+import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_NO_FLASH_FUNCTION;
+import static androidx.exifinterface.media.ExifInterface.GPS_DIRECTION_TRUE;
+import static androidx.exifinterface.media.ExifInterface.GPS_DISTANCE_KILOMETERS;
+import static androidx.exifinterface.media.ExifInterface.GPS_SPEED_KILOMETERS_PER_HOUR;
+import static androidx.exifinterface.media.ExifInterface.LIGHT_SOURCE_FLASH;
+import static androidx.exifinterface.media.ExifInterface.LIGHT_SOURCE_UNKNOWN;
+import static androidx.exifinterface.media.ExifInterface.METERING_MODE_UNKNOWN;
+import static androidx.exifinterface.media.ExifInterface.ORIENTATION_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.RENDERED_PROCESS_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.RESOLUTION_UNIT_INCHES;
+import static androidx.exifinterface.media.ExifInterface.SATURATION_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.SCENE_CAPTURE_TYPE_STANDARD;
+import static androidx.exifinterface.media.ExifInterface.SCENE_TYPE_DIRECTLY_PHOTOGRAPHED;
+import static androidx.exifinterface.media.ExifInterface.SENSITIVITY_TYPE_ISO_SPEED;
+import static androidx.exifinterface.media.ExifInterface.SHARPNESS_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.TAG_APERTURE_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_BRIGHTNESS_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_COLOR_SPACE;
+import static androidx.exifinterface.media.ExifInterface.TAG_COMPONENTS_CONFIGURATION;
+import static androidx.exifinterface.media.ExifInterface.TAG_CONTRAST;
+import static androidx.exifinterface.media.ExifInterface.TAG_CUSTOM_RENDERED;
+import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME;
+import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME_DIGITIZED;
+import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME_ORIGINAL;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXIF_VERSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_BIAS_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_MODE;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_PROGRAM;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_TIME;
+import static androidx.exifinterface.media.ExifInterface.TAG_FILE_SOURCE;
+import static androidx.exifinterface.media.ExifInterface.TAG_FLASH;
+import static androidx.exifinterface.media.ExifInterface.TAG_FLASHPIX_VERSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_FOCAL_LENGTH;
+import static androidx.exifinterface.media.ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT;
+import static androidx.exifinterface.media.ExifInterface.TAG_F_NUMBER;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_DEST_BEARING_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_DEST_DISTANCE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_IMG_DIRECTION_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_SPEED_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_TIMESTAMP;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_TRACK_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_VERSION_ID;
+import static androidx.exifinterface.media.ExifInterface.TAG_IMAGE_LENGTH;
+import static androidx.exifinterface.media.ExifInterface.TAG_IMAGE_WIDTH;
+import static androidx.exifinterface.media.ExifInterface.TAG_INTEROPERABILITY_INDEX;
+import static androidx.exifinterface.media.ExifInterface.TAG_ISO_SPEED_RATINGS;
+import static androidx.exifinterface.media.ExifInterface.TAG_LIGHT_SOURCE;
+import static androidx.exifinterface.media.ExifInterface.TAG_MAKE;
+import static androidx.exifinterface.media.ExifInterface.TAG_MAX_APERTURE_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_METERING_MODE;
+import static androidx.exifinterface.media.ExifInterface.TAG_MODEL;
+import static androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION;
+import static androidx.exifinterface.media.ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY;
+import static androidx.exifinterface.media.ExifInterface.TAG_PIXEL_X_DIMENSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_PIXEL_Y_DIMENSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_RESOLUTION_UNIT;
+import static androidx.exifinterface.media.ExifInterface.TAG_SATURATION;
+import static androidx.exifinterface.media.ExifInterface.TAG_SCENE_CAPTURE_TYPE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SCENE_TYPE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SENSING_METHOD;
+import static androidx.exifinterface.media.ExifInterface.TAG_SENSITIVITY_TYPE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SHARPNESS;
+import static androidx.exifinterface.media.ExifInterface.TAG_SHUTTER_SPEED_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SOFTWARE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME;
+import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME_DIGITIZED;
+import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME_ORIGINAL;
+import static androidx.exifinterface.media.ExifInterface.TAG_WHITE_BALANCE;
+import static androidx.exifinterface.media.ExifInterface.TAG_X_RESOLUTION;
+import static androidx.exifinterface.media.ExifInterface.TAG_Y_CB_CR_POSITIONING;
+import static androidx.exifinterface.media.ExifInterface.TAG_Y_RESOLUTION;
+import static androidx.exifinterface.media.ExifInterface.WHITE_BALANCE_AUTO;
+import static androidx.exifinterface.media.ExifInterface.WHITE_BALANCE_MANUAL;
+import static androidx.exifinterface.media.ExifInterface.Y_CB_CR_POSITIONING_CENTERED;
+
+import android.os.Build;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraCaptureMetaData;
+import androidx.core.util.Preconditions;
+import androidx.exifinterface.media.ExifInterface;
+
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class stores the EXIF header in IFDs according to the JPEG specification.
+ */
+// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}, and is
+// currently expected to be used for writing a subset of Exif values. Support for other mime
+// types besides JPEG have been removed. Support for thumbnails/strips has been removed along
+// with many exif tags. If more tags are required, the source code for ExifInterface should be
+// referenced and can be adapted to this class.
+public class ExifData {
+    private static final String TAG = "ExifData";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Enum representing the white balance mode.
+     */
+    public enum WhiteBalanceMode {
+        /** AWB is turned on. */
+        AUTO,
+        /** AWB is turned off. */
+        MANUAL
+    }
+
+    // Names for the data formats for debugging purpose.
+    static final String[] IFD_FORMAT_NAMES = new String[]{
+            "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
+            "SLONG", "SRATIONAL", "SINGLE", "DOUBLE", "IFD"
+    };
+
+    /**
+     * Private tags used for pointing the other IFD offsets.
+     * The types of the following tags are int.
+     * See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
+     * For SubIFD, see Note 1 of Adobe PageMaker® 6.0 TIFF Technical Notes.
+     */
+    static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer";
+    static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer";
+    static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer";
+    static final String TAG_SUB_IFD_POINTER = "SubIFDPointer";
+
+    // Primary image IFD TIFF tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+    // This is only a subset of the tags defined in ExifInterface
+    private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[]{
+            // For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
+            new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
+            new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
+            new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
+            new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
+            new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
+    };
+
+    // Primary image IFD Exif Private tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+    // This is only a subset of the tags defined in ExifInterface
+    private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[]{
+            new ExifTag(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_F_NUMBER, 33437, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_PHOTOGRAPHIC_SENSITIVITY, 34855, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SENSITIVITY_TYPE, 34864, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING),
+            new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING),
+            new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING),
+            new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING),
+            new ExifTag(TAG_SUBSEC_TIME_ORIGINAL, 37521, IFD_FORMAT_STRING),
+            new ExifTag(TAG_SUBSEC_TIME_DIGITIZED, 37522, IFD_FORMAT_STRING),
+            new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED),
+            new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT),
+            new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT)
+    };
+
+    // Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.6 Tag Support Levels)
+    // This is only a subset of the tags defined in ExifInterface
+    private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[]{
+            new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE),
+            new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING),
+            // Allow SRATIONAL to be compatible with apps using wrong format and
+            // even if it is negative, it may be valid latitude / longitude.
+            new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
+            new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE),
+            new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL),
+            new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING),
+            new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING)
+    };
+
+    // List of tags for pointing to the other image file directory offset.
+    static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[]{
+            new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
+            new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
+    };
+
+    // Primary image IFD Interoperability tag (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+    private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[]{
+            new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING)
+    };
+
+    // List of Exif tag groups
+    static final ExifTag[][] EXIF_TAGS = new ExifTag[][]{
+            IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS
+    };
+
+    // Indices for the above tags. Note these must stay in sync with the order of EXIF_TAGS.
+    static final int IFD_TYPE_PRIMARY = 0;
+    static final int IFD_TYPE_EXIF = 1;
+    static final int IFD_TYPE_GPS = 2;
+    static final int IFD_TYPE_INTEROPERABILITY = 3;
+
+    // NOTE: This is a subset of the tags from ExifInterface. Only supports tags in this class.
+    static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
+            TAG_F_NUMBER, TAG_EXPOSURE_TIME, TAG_GPS_TIMESTAMP));
+
+    private static final int MM_IN_MICRONS = 1000;
+
+    private final List<Map<String, ExifAttribute>> mAttributes;
+    private final ByteOrder mByteOrder;
+
+    ExifData(ByteOrder order, List<Map<String, ExifAttribute>> attributes) {
+        Preconditions.checkState(attributes.size() == EXIF_TAGS.length, "Malformed attributes "
+                + "list. Number of IFDs mismatch.");
+        mByteOrder = order;
+        mAttributes = attributes;
+    }
+
+    /**
+     * Gets the byte order.
+     */
+    @NonNull
+    public ByteOrder getByteOrder() {
+        return mByteOrder;
+    }
+
+    @NonNull
+    Map<String, ExifAttribute> getAttributes(int ifdIndex) {
+        Preconditions.checkArgumentInRange(ifdIndex, 0, EXIF_TAGS.length,
+                "Invalid IFD index: " + ifdIndex + ". Index should be between [0, EXIF_TAGS"
+                        + ".length] ");
+        return mAttributes.get(ifdIndex);
+    }
+
+    /**
+     * Returns the value of the specified tag or {@code null} if there
+     * is no such tag in the image file.
+     *
+     * @param tag the name of the tag.
+     */
+    @Nullable
+    public String getAttribute(@NonNull String tag) {
+        ExifAttribute attribute = getExifAttribute(tag);
+        if (attribute != null) {
+            if (!sTagSetForCompatibility.contains(tag)) {
+                return attribute.getStringValue(mByteOrder);
+            }
+            if (tag.equals(TAG_GPS_TIMESTAMP)) {
+                // Convert the rational values to the custom formats for backwards compatibility.
+                if (attribute.format != IFD_FORMAT_URATIONAL
+                        && attribute.format != IFD_FORMAT_SRATIONAL) {
+                    Logger.w(TAG,
+                            "GPS Timestamp format is not rational. format=" + attribute.format);
+                    return null;
+                }
+                LongRational[] array =
+                        (LongRational[]) attribute.getValue(mByteOrder);
+                if (array == null || array.length != 3) {
+                    Logger.w(TAG, "Invalid GPS Timestamp array. array=" + Arrays.toString(array));
+                    return null;
+                }
+                return String.format(Locale.US, "%02d:%02d:%02d",
+                        (int) ((float) array[0].getNumerator() / array[0].getDenominator()),
+                        (int) ((float) array[1].getNumerator() / array[1].getDenominator()),
+                        (int) ((float) array[2].getNumerator() / array[2].getDenominator()));
+            }
+            try {
+                return Double.toString(attribute.getDoubleValue(mByteOrder));
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag.
+     *
+     * @param tag the name of the tag.
+     */
+    @SuppressWarnings("deprecation")
+    @Nullable
+    private ExifAttribute getExifAttribute(@NonNull String tag) {
+        // Maintain compatibility.
+        if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+            if (DEBUG) {
+                Logger.d(TAG, "getExifAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+                        + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+            }
+            tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+        }
+        // Retrieves all tag groups. The value from primary image tag group has a higher priority
+        // than the value from the thumbnail tag group if there are more than one candidates.
+        for (int i = 0; i < EXIF_TAGS.length; ++i) {
+            ExifAttribute value = mAttributes.get(i).get(tag);
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Generates an empty builder suitable for generating ExifData for JPEG from the current device.
+     */
+    @NonNull
+    public static Builder builderForDevice() {
+        // Add PRIMARY defaults. EXIF and GPS defaults will be added in build()
+        return new Builder(ByteOrder.BIG_ENDIAN)
+                .setAttribute(TAG_ORIENTATION, String.valueOf(ORIENTATION_NORMAL))
+                .setAttribute(TAG_X_RESOLUTION, "72/1")
+                .setAttribute(TAG_Y_RESOLUTION, "72/1")
+                .setAttribute(TAG_RESOLUTION_UNIT, String.valueOf(RESOLUTION_UNIT_INCHES))
+                .setAttribute(TAG_Y_CB_CR_POSITIONING,
+                        String.valueOf(Y_CB_CR_POSITIONING_CENTERED))
+                // Defaults derived from device
+                .setAttribute(TAG_MAKE, Build.MANUFACTURER)
+                .setAttribute(TAG_MODEL, Build.MODEL);
+    }
+
+    /**
+     * Builder for the {@link ExifData} class.
+     */
+    public static final class Builder {
+        // Pattern to check gps timestamp
+        private static final Pattern GPS_TIMESTAMP_PATTERN =
+                Pattern.compile("^(\\d{2}):(\\d{2}):(\\d{2})$");
+        // Pattern to check date time primary format (e.g. 2020:01:01 00:00:00)
+        private static final Pattern DATETIME_PRIMARY_FORMAT_PATTERN =
+                Pattern.compile("^(\\d{4}):(\\d{2}):(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+        // Pattern to check date time secondary format (e.g. 2020-01-01 00:00:00)
+        private static final Pattern DATETIME_SECONDARY_FORMAT_PATTERN =
+                Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+        private static final int DATETIME_VALUE_STRING_LENGTH = 19;
+
+        // Mappings from tag name to tag number and each item represents one IFD tag group.
+        static final List<HashMap<String, ExifTag>> sExifTagMapsForWriting =
+                Collections.list(new Enumeration<HashMap<String, ExifTag>>() {
+                    int mIfdIndex = 0;
+
+                    @Override
+                    public boolean hasMoreElements() {
+                        return mIfdIndex < EXIF_TAGS.length;
+                    }
+
+                    @Override
+                    public HashMap<String, ExifTag> nextElement() {
+                        // Build up the hash tables to look up Exif tags for writing Exif tags.
+                        HashMap<String, ExifTag> map = new HashMap<>();
+                        for (ExifTag tag : EXIF_TAGS[mIfdIndex]) {
+                            map.put(tag.name, tag);
+                        }
+                        mIfdIndex++;
+                        return map;
+                    }
+                });
+
+        final List<Map<String, ExifAttribute>> mAttributes = Collections.list(
+                new Enumeration<Map<String, ExifAttribute>>() {
+                    int mIfdIndex = 0;
+
+                    @Override
+                    public boolean hasMoreElements() {
+                        return mIfdIndex < EXIF_TAGS.length;
+                    }
+
+                    @Override
+                    public Map<String, ExifAttribute> nextElement() {
+                        mIfdIndex++;
+                        return new HashMap<>();
+                    }
+                });
+        private final ByteOrder mByteOrder;
+
+        Builder(@NonNull ByteOrder byteOrder) {
+            mByteOrder = byteOrder;
+        }
+
+        /**
+         * Sets the width of the image.
+         *
+         * @param width the width of the image.
+         */
+        @NonNull
+        public Builder setImageWidth(int width) {
+            return setAttribute(TAG_IMAGE_WIDTH, String.valueOf(width));
+        }
+
+        /**
+         * Sets the height of the image.
+         *
+         * @param height the height of the image.
+         */
+        @NonNull
+        public Builder setImageHeight(int height) {
+            return setAttribute(TAG_IMAGE_LENGTH, String.valueOf(height));
+        }
+
+        /**
+         * Sets the orientation of the image in degrees.
+         *
+         * @param orientationDegrees the orientation in degrees. Can be one of (0, 90, 180, 270)
+         */
+        @NonNull
+        public Builder setOrientationDegrees(int orientationDegrees) {
+            int orientationEnum;
+            switch (orientationDegrees) {
+                case 0:
+                    orientationEnum = ExifInterface.ORIENTATION_NORMAL;
+                    break;
+                case 90:
+                    orientationEnum = ExifInterface.ORIENTATION_ROTATE_90;
+                    break;
+                case 180:
+                    orientationEnum = ExifInterface.ORIENTATION_ROTATE_180;
+                    break;
+                case 270:
+                    orientationEnum = ExifInterface.ORIENTATION_ROTATE_270;
+                    break;
+                default:
+                    Logger.w(TAG,
+                            "Unexpected orientation value: " + orientationDegrees
+                                    + ". Must be one of 0, 90, 180, 270.");
+                    orientationEnum = ExifInterface.ORIENTATION_UNDEFINED;
+                    break;
+            }
+            return setAttribute(TAG_ORIENTATION, String.valueOf(orientationEnum));
+        }
+
+        /**
+         * Sets the flash information from
+         * {@link androidx.camera.core.impl.CameraCaptureMetaData.FlashState}.
+         *
+         * @param flashState the state of the flash at capture time.
+         */
+        @NonNull
+        public Builder setFlashState(@NonNull CameraCaptureMetaData.FlashState flashState) {
+            if (flashState == CameraCaptureMetaData.FlashState.UNKNOWN) {
+                // Cannot set flash state information
+                return this;
+            }
+
+            short value;
+            switch (flashState) {
+                case READY:
+                    value = 0;
+                    break;
+                case NONE:
+                    value = FLAG_FLASH_NO_FLASH_FUNCTION;
+                    break;
+                case FIRED:
+                    value = FLAG_FLASH_FIRED;
+                    break;
+                default:
+                    Logger.w(TAG, "Unknown flash state: " + flashState);
+                    return this;
+            }
+
+            if ((value & FLAG_FLASH_FIRED) == FLAG_FLASH_FIRED) {
+                // Set light source to flash
+                setAttribute(TAG_LIGHT_SOURCE, String.valueOf(LIGHT_SOURCE_FLASH));
+            }
+
+
+            return setAttribute(TAG_FLASH, String.valueOf(value));
+        }
+
+        /**
+         * Sets the amount of time the sensor was exposed for, in nanoseconds.
+         * @param exposureTimeNs The exposure time in nanoseconds.
+         */
+        @NonNull
+        public Builder setExposureTimeNanos(long exposureTimeNs) {
+            return setAttribute(TAG_EXPOSURE_TIME,
+                    String.valueOf(exposureTimeNs / (double) TimeUnit.SECONDS.toNanos(1)));
+        }
+
+        /**
+         * Sets the lens f-number.
+         *
+         * <p>The lens f-number has precision 1.xx, for example, 1.80.
+         * @param fNumber The f-number.
+         */
+        @NonNull
+        public Builder setLensFNumber(float fNumber) {
+            return setAttribute(TAG_F_NUMBER, String.valueOf(fNumber));
+        }
+
+        /**
+         * Sets the ISO.
+         *
+         * @param iso the standard ISO sensitivity value, as defined in ISO 12232:2006.
+         */
+        @NonNull
+        public Builder setIso(int iso) {
+            return setAttribute(TAG_SENSITIVITY_TYPE, String.valueOf(SENSITIVITY_TYPE_ISO_SPEED))
+                    .setAttribute(TAG_PHOTOGRAPHIC_SENSITIVITY, String.valueOf(Math.min(65535,
+                            iso)));
+        }
+
+        /**
+         * Sets lens focal length, in millimeters.
+         *
+         * @param focalLength The lens focal length in millimeters.
+         */
+        @NonNull
+        public Builder setFocalLength(float focalLength) {
+            LongRational focalLengthRational =
+                    new LongRational((long) (focalLength * MM_IN_MICRONS), MM_IN_MICRONS);
+            return setAttribute(TAG_FOCAL_LENGTH, focalLengthRational.toString());
+        }
+
+        /**
+         * Sets the white balance mode.
+         *
+         * @param whiteBalanceMode The white balance mode. One of {@link WhiteBalanceMode#AUTO}
+         *                        or {@link WhiteBalanceMode#MANUAL}.
+         */
+        @NonNull
+        public Builder setWhiteBalanceMode(@NonNull WhiteBalanceMode whiteBalanceMode) {
+            String wbString = null;
+            switch (whiteBalanceMode) {
+                case AUTO:
+                    wbString = String.valueOf(WHITE_BALANCE_AUTO);
+                    break;
+                case MANUAL:
+                    wbString = String.valueOf(WHITE_BALANCE_MANUAL);
+                    break;
+            }
+            return setAttribute(TAG_WHITE_BALANCE, wbString);
+        }
+
+        /**
+         * Sets the value of the specified tag.
+         *
+         * @param tag   the name of the tag.
+         * @param value the value of the tag.
+         */
+        @NonNull
+        public Builder setAttribute(@NonNull String tag, @NonNull String value) {
+            setAttributeInternal(tag, value, mAttributes);
+            return this;
+        }
+
+        /**
+         * Removes the attribute with the given tag.
+         *
+         * @param tag the name of the tag.
+         */
+        @NonNull
+        public Builder removeAttribute(@NonNull String tag) {
+            setAttributeInternal(tag, null, mAttributes);
+            return this;
+        }
+
+        private void setAttributeIfMissing(@NonNull String tag, @NonNull String value,
+                @NonNull List<Map<String, ExifAttribute>> attributes) {
+            for (Map<String, ExifAttribute> attrs : attributes) {
+                if (attrs.containsKey(tag)) {
+                    // Attr already exists
+                    return;
+                }
+            }
+
+            // Add missing attribute.
+            setAttributeInternal(tag, value, attributes);
+        }
+
+        @SuppressWarnings("deprecation")
+        // Allows null values to remove attributes
+        private void setAttributeInternal(@NonNull String tag, @Nullable String value,
+                @NonNull List<Map<String, ExifAttribute>> attributes) {
+            // Validate and convert if necessary.
+            if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
+                    || TAG_DATETIME_DIGITIZED.equals(tag)) {
+                if (value != null) {
+                    boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
+                    boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(
+                            value).find();
+                    // Validate
+                    if (value.length() != DATETIME_VALUE_STRING_LENGTH
+                            || (!isPrimaryFormat && !isSecondaryFormat)) {
+                        Logger.w(TAG, "Invalid value for " + tag + " : " + value);
+                        return;
+                    }
+                    // If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it
+                    // to primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
+                    // official documentation.
+                    // See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
+                    if (isSecondaryFormat) {
+                        // Replace "-" with ":" to match the primary format.
+                        value = value.replaceAll("-", ":");
+                    }
+                }
+            }
+            // Maintain compatibility.
+            if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+                if (DEBUG) {
+                    Logger.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+                            + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+                }
+                tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+            }
+            // Convert the given value to rational values for backwards compatibility.
+            if (value != null && sTagSetForCompatibility.contains(tag)) {
+                if (tag.equals(TAG_GPS_TIMESTAMP)) {
+                    Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
+                    if (!m.find()) {
+                        Logger.w(TAG, "Invalid value for " + tag + " : " + value);
+                        return;
+                    }
+                    value = Integer.parseInt(Preconditions.checkNotNull(m.group(1))) + "/1,"
+                            + Integer.parseInt(Preconditions.checkNotNull(m.group(2))) + "/1,"
+                            + Integer.parseInt(Preconditions.checkNotNull(m.group(3))) + "/1";
+                } else {
+                    try {
+                        double doubleValue = Double.parseDouble(value);
+                        value = new LongRational(doubleValue).toString();
+                    } catch (NumberFormatException e) {
+                        Logger.w(TAG, "Invalid value for " + tag + " : " + value, e);
+                        return;
+                    }
+                }
+            }
+
+            for (int i = 0; i < EXIF_TAGS.length; ++i) {
+                final ExifTag exifTag = sExifTagMapsForWriting.get(i).get(tag);
+                if (exifTag != null) {
+                    if (value == null) {
+                        attributes.get(i).remove(tag);
+                        continue;
+                    }
+                    Pair<Integer, Integer> guess = guessDataFormat(value);
+                    int dataFormat;
+                    if (exifTag.primaryFormat == guess.first
+                            || exifTag.primaryFormat == guess.second) {
+                        dataFormat = exifTag.primaryFormat;
+                    } else if (exifTag.secondaryFormat != -1 && (
+                            exifTag.secondaryFormat == guess.first
+                                    || exifTag.secondaryFormat == guess.second)) {
+                        dataFormat = exifTag.secondaryFormat;
+                    } else if (exifTag.primaryFormat == IFD_FORMAT_BYTE
+                            || exifTag.primaryFormat == IFD_FORMAT_UNDEFINED
+                            || exifTag.primaryFormat == IFD_FORMAT_STRING) {
+                        dataFormat = exifTag.primaryFormat;
+                    } else {
+                        if (DEBUG) {
+                            Logger.d(TAG, "Given tag (" + tag
+                                    + ") value didn't match with one of expected "
+                                    + "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat]
+                                    + (exifTag.secondaryFormat == -1 ? "" : ", "
+                                    + IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: "
+                                    + IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? ""
+                                    : ", "
+                                            + IFD_FORMAT_NAMES[guess.second]) + ")");
+                        }
+                        continue;
+                    }
+                    switch (dataFormat) {
+                        case IFD_FORMAT_BYTE: {
+                            attributes.get(i).put(tag, ExifAttribute.createByte(value));
+                            break;
+                        }
+                        case IFD_FORMAT_UNDEFINED:
+                        case IFD_FORMAT_STRING: {
+                            attributes.get(i).put(tag, ExifAttribute.createString(value));
+                            break;
+                        }
+                        case IFD_FORMAT_USHORT: {
+                            final String[] values = value.split(",", -1);
+                            final int[] intArray = new int[values.length];
+                            for (int j = 0; j < values.length; ++j) {
+                                intArray[j] = Integer.parseInt(values[j]);
+                            }
+                            attributes.get(i).put(tag,
+                                    ExifAttribute.createUShort(intArray, mByteOrder));
+                            break;
+                        }
+                        case IFD_FORMAT_SLONG: {
+                            final String[] values = value.split(",", -1);
+                            final int[] intArray = new int[values.length];
+                            for (int j = 0; j < values.length; ++j) {
+                                intArray[j] = Integer.parseInt(values[j]);
+                            }
+                            attributes.get(i).put(tag,
+                                    ExifAttribute.createSLong(intArray, mByteOrder));
+                            break;
+                        }
+                        case IFD_FORMAT_ULONG: {
+                            final String[] values = value.split(",", -1);
+                            final long[] longArray = new long[values.length];
+                            for (int j = 0; j < values.length; ++j) {
+                                longArray[j] = Long.parseLong(values[j]);
+                            }
+                            attributes.get(i).put(tag,
+                                    ExifAttribute.createULong(longArray, mByteOrder));
+                            break;
+                        }
+                        case IFD_FORMAT_URATIONAL: {
+                            final String[] values = value.split(",", -1);
+                            final LongRational[] rationalArray = new LongRational[values.length];
+                            for (int j = 0; j < values.length; ++j) {
+                                final String[] numbers = values[j].split("/", -1);
+                                rationalArray[j] = new LongRational(
+                                        (long) Double.parseDouble(numbers[0]),
+                                        (long) Double.parseDouble(numbers[1]));
+                            }
+                            attributes.get(i).put(tag,
+                                    ExifAttribute.createURational(rationalArray, mByteOrder));
+                            break;
+                        }
+                        case IFD_FORMAT_SRATIONAL: {
+                            final String[] values = value.split(",", -1);
+                            final LongRational[] rationalArray = new LongRational[values.length];
+                            for (int j = 0; j < values.length; ++j) {
+                                final String[] numbers = values[j].split("/", -1);
+                                rationalArray[j] = new LongRational(
+                                        (long) Double.parseDouble(numbers[0]),
+                                        (long) Double.parseDouble(numbers[1]));
+                            }
+                            attributes.get(i).put(tag,
+                                    ExifAttribute.createSRational(rationalArray, mByteOrder));
+                            break;
+                        }
+                        case IFD_FORMAT_DOUBLE: {
+                            final String[] values = value.split(",", -1);
+                            final double[] doubleArray = new double[values.length];
+                            for (int j = 0; j < values.length; ++j) {
+                                doubleArray[j] = Double.parseDouble(values[j]);
+                            }
+                            attributes.get(i).put(tag,
+                                    ExifAttribute.createDouble(doubleArray, mByteOrder));
+                            break;
+                        }
+                        default:
+                            if (DEBUG) {
+                                Logger.d(TAG,
+                                        "Data format isn't one of expected formats: " + dataFormat);
+                            }
+                    }
+                }
+            }
+        }
+
+        /**
+         * Builds an {@link ExifData} from the current state of the builder.
+         */
+        @NonNull
+        public ExifData build() {
+            // Create a read-only copy of all attributes. This needs to be a deep copy since
+            // build() can be called multiple times. We'll remove null values as well.
+            List<Map<String, ExifAttribute>> attributes = Collections.list(
+                    new Enumeration<Map<String, ExifAttribute>>() {
+                        final Enumeration<Map<String, ExifAttribute>> mMapEnumeration =
+                                Collections.enumeration(mAttributes);
+
+                        @Override
+                        public boolean hasMoreElements() {
+                            return mMapEnumeration.hasMoreElements();
+                        }
+
+                        @Override
+                        public Map<String, ExifAttribute> nextElement() {
+                            return new HashMap<>(mMapEnumeration.nextElement());
+                        }
+                    });
+            // Add EXIF defaults if needed
+            if (!attributes.get(IFD_TYPE_EXIF).isEmpty()) {
+                setAttributeIfMissing(TAG_EXPOSURE_PROGRAM,
+                        String.valueOf(EXPOSURE_PROGRAM_NOT_DEFINED), attributes);
+                setAttributeIfMissing(TAG_EXIF_VERSION, "0230", attributes);
+                // Default is for YCbCr components
+                setAttributeIfMissing(TAG_COMPONENTS_CONFIGURATION, "1,2,3,0", attributes);
+                setAttributeIfMissing(TAG_METERING_MODE, String.valueOf(METERING_MODE_UNKNOWN),
+                        attributes);
+                setAttributeIfMissing(TAG_LIGHT_SOURCE, String.valueOf(LIGHT_SOURCE_UNKNOWN),
+                        attributes);
+                setAttributeIfMissing(TAG_FLASHPIX_VERSION, "0100", attributes);
+                setAttributeIfMissing(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+                        String.valueOf(RESOLUTION_UNIT_INCHES), attributes);
+                setAttributeIfMissing(TAG_FILE_SOURCE, String.valueOf(FILE_SOURCE_DSC), attributes);
+                setAttributeIfMissing(TAG_SCENE_TYPE,
+                        String.valueOf(SCENE_TYPE_DIRECTLY_PHOTOGRAPHED), attributes);
+                setAttributeIfMissing(TAG_CUSTOM_RENDERED, String.valueOf(RENDERED_PROCESS_NORMAL),
+                        attributes);
+                setAttributeIfMissing(TAG_SCENE_CAPTURE_TYPE,
+                        String.valueOf(SCENE_CAPTURE_TYPE_STANDARD), attributes);
+                setAttributeIfMissing(TAG_CONTRAST, String.valueOf(CONTRAST_NORMAL), attributes);
+                setAttributeIfMissing(TAG_SATURATION, String.valueOf(SATURATION_NORMAL),
+                        attributes);
+                setAttributeIfMissing(TAG_SHARPNESS, String.valueOf(SHARPNESS_NORMAL), attributes);
+            }
+            // Add GPS defaults if needed
+            if (!attributes.get(IFD_TYPE_GPS).isEmpty()) {
+                setAttributeIfMissing(TAG_GPS_VERSION_ID, "2300", attributes);
+                setAttributeIfMissing(TAG_GPS_SPEED_REF, GPS_SPEED_KILOMETERS_PER_HOUR, attributes);
+                setAttributeIfMissing(TAG_GPS_TRACK_REF, GPS_DIRECTION_TRUE, attributes);
+                setAttributeIfMissing(TAG_GPS_IMG_DIRECTION_REF, GPS_DIRECTION_TRUE, attributes);
+                setAttributeIfMissing(TAG_GPS_DEST_BEARING_REF, GPS_DIRECTION_TRUE, attributes);
+                setAttributeIfMissing(TAG_GPS_DEST_DISTANCE_REF, GPS_DISTANCE_KILOMETERS,
+                        attributes);
+            }
+            return new ExifData(mByteOrder, attributes);
+        }
+
+        /**
+         * Determines the data format of EXIF entry value.
+         *
+         * @param entryValue The value to be determined.
+         * @return Returns two data formats guessed as a pair in integer. If there is no two
+         * candidate
+         * data formats for the given entry value, returns {@code -1} in the second of the pair.
+         */
+        private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
+            // See TIFF 6.0 Section 2, "Image File Directory".
+            // Take the first component if there are more than one component.
+            if (entryValue.contains(",")) {
+                String[] entryValues = entryValue.split(",", -1);
+                Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
+                if (dataFormat.first == IFD_FORMAT_STRING) {
+                    return dataFormat;
+                }
+                for (int i = 1; i < entryValues.length; ++i) {
+                    final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
+                    int first = -1, second = -1;
+                    if (guessDataFormat.first.equals(dataFormat.first)
+                            || guessDataFormat.second.equals(dataFormat.first)) {
+                        first = dataFormat.first;
+                    }
+                    if (dataFormat.second != -1 && (guessDataFormat.first.equals(dataFormat.second)
+                            || guessDataFormat.second.equals(dataFormat.second))) {
+                        second = dataFormat.second;
+                    }
+                    if (first == -1 && second == -1) {
+                        return new Pair<>(IFD_FORMAT_STRING, -1);
+                    }
+                    if (first == -1) {
+                        dataFormat = new Pair<>(second, -1);
+                        continue;
+                    }
+                    if (second == -1) {
+                        dataFormat = new Pair<>(first, -1);
+                    }
+                }
+                return dataFormat;
+            }
+
+            if (entryValue.contains("/")) {
+                String[] rationalNumber = entryValue.split("/", -1);
+                if (rationalNumber.length == 2) {
+                    try {
+                        long numerator = (long) Double.parseDouble(rationalNumber[0]);
+                        long denominator = (long) Double.parseDouble(rationalNumber[1]);
+                        if (numerator < 0L || denominator < 0L) {
+                            return new Pair<>(IFD_FORMAT_SRATIONAL, -1);
+                        }
+                        if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) {
+                            return new Pair<>(IFD_FORMAT_URATIONAL, -1);
+                        }
+                        return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL);
+                    } catch (NumberFormatException e) {
+                        // Ignored
+                    }
+                }
+                return new Pair<>(IFD_FORMAT_STRING, -1);
+            }
+            try {
+                long longValue = Long.parseLong(entryValue);
+                if (longValue >= 0 && longValue <= 65535) {
+                    return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG);
+                }
+                if (longValue < 0) {
+                    return new Pair<>(IFD_FORMAT_SLONG, -1);
+                }
+                return new Pair<>(IFD_FORMAT_ULONG, -1);
+            } catch (NumberFormatException e) {
+                // Ignored
+            }
+            try {
+                Double.parseDouble(entryValue);
+                return new Pair<>(IFD_FORMAT_DOUBLE, -1);
+            } catch (NumberFormatException e) {
+                // Ignored
+            }
+            return new Pair<>(IFD_FORMAT_STRING, -1);
+        }
+    }
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifOutputStream.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifOutputStream.java
new file mode 100644
index 0000000..a2594fd
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifOutputStream.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import static androidx.camera.core.impl.utils.ExifAttribute.ASCII;
+import static androidx.camera.core.impl.utils.ExifData.Builder.sExifTagMapsForWriting;
+import static androidx.camera.core.impl.utils.ExifData.EXIF_POINTER_TAGS;
+import static androidx.camera.core.impl.utils.ExifData.EXIF_TAGS;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_EXIF;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_GPS;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_INTEROPERABILITY;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_PRIMARY;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.Logger;
+import androidx.core.util.Preconditions;
+
+import java.io.BufferedOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * This class provides a way to replace the Exif header of a JPEG image.
+ * <p>
+ * Below is an example of writing EXIF data into a file
+ *
+ * <pre>
+ * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
+ *     OutputStream os = null;
+ *     try {
+ *         os = new FileOutputStream(path);
+ *         // Set the exif header on the output stream
+ *         ExifOutputStream eos = new ExifOutputStream(os, exif);
+ *         // Write the original jpeg out, the header will be added into the file.
+ *         eos.write(jpeg);
+ *     } catch (FileNotFoundException e) {
+ *         e.printStackTrace();
+ *     } catch (IOException e) {
+ *         e.printStackTrace();
+ *     } finally {
+ *         if (os != null) {
+ *             try {
+ *                 os.close();
+ *             } catch (IOException e) {
+ *                 e.printStackTrace();
+ *             }
+ *         }
+ *     }
+ * }
+ * </pre>
+ */
+public final class ExifOutputStream extends FilterOutputStream {
+    private static final String TAG = "ExifOutputStream";
+    private static final boolean DEBUG = false;
+    private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
+
+    private static final int STATE_SOI = 0;
+    private static final int STATE_FRAME_HEADER = 1;
+    private static final int STATE_JPEG_DATA = 2;
+
+    // Identifier for EXIF APP1 segment in JPEG
+    private static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(ASCII);
+
+    // Types of Exif byte alignments (see JEITA CP-3451C Section 4.5.2)
+    private static final short BYTE_ALIGN_II = 0x4949;  // II: Intel order
+    private static final short BYTE_ALIGN_MM = 0x4d4d;  // MM: Motorola order
+
+    // TIFF Header Fixed Constant (see JEITA CP-3451C Section 4.5.2)
+    private static final byte START_CODE = 0x2a; // 42
+    private static final int IFD_OFFSET = 8;
+
+    private final ExifData mExifData;
+    private final byte[] mSingleByteArray = new byte[1];
+    private final ByteBuffer mBuffer = ByteBuffer.allocate(4);
+    private int mState = STATE_SOI;
+    private int mByteToSkip;
+    private int mByteToCopy;
+
+    /**
+     * Creates an ExifOutputStream that wraps the given {@link OutputStream} and overwrites exif
+     * with the provided {@link ExifData}.
+     * @param ou OutputStream which will be sent the final output.
+     * @param exifData Exif data which will overwrite any exif data sent to this stream.
+     */
+    public ExifOutputStream(@NonNull OutputStream ou, @NonNull ExifData exifData) {
+        super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
+        mExifData = exifData;
+    }
+
+    private int requestByteToBuffer(int requestByteCount, byte[] buffer, int offset, int length) {
+        int byteNeeded = requestByteCount - mBuffer.position();
+        int byteToRead = Math.min(length, byteNeeded);
+        mBuffer.put(buffer, offset, byteToRead);
+        return byteToRead;
+    }
+
+    /**
+     * Writes the image out. The input data should be a valid JPEG format. After
+     * writing, it's Exif header will be replaced by the given header.
+     */
+    @Override
+    public void write(@NonNull byte[] buffer, int offset, int length) throws IOException {
+        while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
+                && length > 0) {
+            if (mByteToSkip > 0) {
+                int byteToProcess = Math.min(length, mByteToSkip);
+                length -= byteToProcess;
+                mByteToSkip -= byteToProcess;
+                offset += byteToProcess;
+            }
+            if (mByteToCopy > 0) {
+                int byteToProcess = Math.min(length, mByteToCopy);
+                out.write(buffer, offset, byteToProcess);
+                length -= byteToProcess;
+                mByteToCopy -= byteToProcess;
+                offset += byteToProcess;
+            }
+            if (length == 0) {
+                return;
+            }
+            switch (mState) {
+                case STATE_SOI:
+                    int byteRead = requestByteToBuffer(2, buffer, offset, length);
+                    offset += byteRead;
+                    length -= byteRead;
+                    if (mBuffer.position() < 2) {
+                        return;
+                    }
+                    mBuffer.rewind();
+                    if (mBuffer.getShort() != JpegHeader.SOI) {
+                        throw new IOException("Not a valid jpeg image, cannot write exif");
+                    }
+                    out.write(mBuffer.array(), 0, 2);
+                    mState = STATE_FRAME_HEADER;
+                    mBuffer.rewind();
+                    ByteOrderedDataOutputStream dataOutputStream =
+                            new ByteOrderedDataOutputStream(out, ByteOrder.BIG_ENDIAN);
+                    dataOutputStream.writeShort(JpegHeader.APP1);
+                    writeExifSegment(dataOutputStream);
+                    break;
+                case STATE_FRAME_HEADER:
+                    // We ignore the APP1 segment and copy all other segments
+                    // until SOF tag.
+                    byteRead = requestByteToBuffer(4, buffer, offset, length);
+                    offset += byteRead;
+                    length -= byteRead;
+                    // Check if this image data doesn't contain SOF.
+                    if (mBuffer.position() == 2) {
+                        short tag = mBuffer.getShort();
+                        if (tag == JpegHeader.EOI) {
+                            out.write(mBuffer.array(), 0, 2);
+                            mBuffer.rewind();
+                        }
+                    }
+                    if (mBuffer.position() < 4) {
+                        return;
+                    }
+                    mBuffer.rewind();
+                    short marker = mBuffer.getShort();
+                    if (marker == JpegHeader.APP1) {
+                        mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
+                        mState = STATE_JPEG_DATA;
+                    } else if (!JpegHeader.isSofMarker(marker)) {
+                        out.write(mBuffer.array(), 0, 4);
+                        mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
+                    } else {
+                        out.write(mBuffer.array(), 0, 4);
+                        mState = STATE_JPEG_DATA;
+                    }
+                    mBuffer.rewind();
+            }
+        }
+        if (length > 0) {
+            out.write(buffer, offset, length);
+        }
+    }
+
+    /**
+     * Writes the one bytes out. The input data should be a valid JPEG format.
+     * After writing, it's Exif header will be replaced by the given header.
+     */
+    @Override
+    public void write(int oneByte) throws IOException {
+        mSingleByteArray[0] = (byte) (0xff & oneByte);
+        write(mSingleByteArray);
+    }
+
+    /**
+     * Equivalent to calling write(buffer, 0, buffer.length).
+     */
+    @Override
+    public void write(@NonNull byte[] buffer) throws IOException {
+        write(buffer, 0, buffer.length);
+    }
+
+    // Writes an Exif segment into the given output stream.
+    private void writeExifSegment(@NonNull ByteOrderedDataOutputStream dataOutputStream)
+            throws IOException {
+        // The following variables are for calculating each IFD tag group size in bytes.
+        int[] ifdOffsets = new int[EXIF_TAGS.length];
+        int[] ifdDataSizes = new int[EXIF_TAGS.length];
+
+        // Remove IFD pointer tags (we'll re-add it later.)
+        for (ExifTag tag : EXIF_POINTER_TAGS) {
+            for (int ifdIndex = 0; ifdIndex < EXIF_TAGS.length; ++ifdIndex) {
+                mExifData.getAttributes(ifdIndex).remove(tag.name);
+            }
+        }
+
+        // Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
+        // offset when there is one or more tags in the thumbnail IFD.
+        if (!mExifData.getAttributes(IFD_TYPE_EXIF).isEmpty()) {
+            mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[1].name,
+                    ExifAttribute.createULong(0, mExifData.getByteOrder()));
+        }
+        if (!mExifData.getAttributes(IFD_TYPE_GPS).isEmpty()) {
+            mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[2].name,
+                    ExifAttribute.createULong(0, mExifData.getByteOrder()));
+        }
+        if (!mExifData.getAttributes(IFD_TYPE_INTEROPERABILITY).isEmpty()) {
+            mExifData.getAttributes(IFD_TYPE_EXIF).put(EXIF_POINTER_TAGS[3].name,
+                    ExifAttribute.createULong(0, mExifData.getByteOrder()));
+        }
+
+        // Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
+        // value which has a bigger size than 4 bytes.
+        for (int i = 0; i < EXIF_TAGS.length; ++i) {
+            int sum = 0;
+            for (Map.Entry<String, ExifAttribute> entry : mExifData.getAttributes(i).entrySet()) {
+                final ExifAttribute exifAttribute = entry.getValue();
+                final int size = exifAttribute.size();
+                if (size > 4) {
+                    sum += size;
+                }
+            }
+            ifdDataSizes[i] += sum;
+        }
+
+        // Calculate IFD offsets.
+        // 8 bytes are for TIFF headers: 2 bytes (byte order) + 2 bytes (identifier) + 4 bytes
+        // (offset of IFDs)
+        int position = 8;
+        for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+            if (!mExifData.getAttributes(ifdType).isEmpty()) {
+                ifdOffsets[ifdType] = position;
+                position += 2 + mExifData.getAttributes(ifdType).size() * 12 + 4
+                        + ifdDataSizes[ifdType];
+            }
+        }
+
+        int totalSize = position;
+        // Add 8 bytes for APP1 size and identifier data
+        totalSize += 8;
+        if (DEBUG) {
+            for (int i = 0; i < EXIF_TAGS.length; ++i) {
+                Logger.d(TAG, String.format(Locale.US, "index: %d, offsets: %d, tag count: %d, "
+                                + "data sizes: %d, total size: %d", i, ifdOffsets[i],
+                        mExifData.getAttributes(i).size(),
+                        ifdDataSizes[i], totalSize));
+            }
+        }
+
+        // Update IFD pointer tags with the calculated offsets.
+        if (!mExifData.getAttributes(IFD_TYPE_EXIF).isEmpty()) {
+            mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[1].name,
+                    ExifAttribute.createULong(ifdOffsets[IFD_TYPE_EXIF], mExifData.getByteOrder()));
+        }
+        if (!mExifData.getAttributes(IFD_TYPE_GPS).isEmpty()) {
+            mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[2].name,
+                    ExifAttribute.createULong(ifdOffsets[IFD_TYPE_GPS], mExifData.getByteOrder()));
+        }
+        if (!mExifData.getAttributes(IFD_TYPE_INTEROPERABILITY).isEmpty()) {
+            mExifData.getAttributes(IFD_TYPE_EXIF).put(EXIF_POINTER_TAGS[3].name,
+                    ExifAttribute.createULong(
+                            ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifData.getByteOrder()));
+        }
+
+        // Write JPEG specific data (APP1 size, APP1 identifier)
+        dataOutputStream.writeUnsignedShort(totalSize);
+        dataOutputStream.write(IDENTIFIER_EXIF_APP1);
+
+        // Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
+        dataOutputStream.writeShort(mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN
+                ? BYTE_ALIGN_MM : BYTE_ALIGN_II);
+        dataOutputStream.setByteOrder(mExifData.getByteOrder());
+        dataOutputStream.writeUnsignedShort(START_CODE);
+        dataOutputStream.writeUnsignedInt(IFD_OFFSET);
+
+        // Write IFD groups. See JEITA CP-3451C Section 4.5.8. Figure 9.
+        for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+            if (!mExifData.getAttributes(ifdType).isEmpty()) {
+                // See JEITA CP-3451C Section 4.6.2: IFD structure.
+                // Write entry count
+                dataOutputStream.writeUnsignedShort(mExifData.getAttributes(ifdType).size());
+
+                // Write entry info
+                int dataOffset = ifdOffsets[ifdType] + 2 + mExifData.getAttributes(ifdType).size()
+                        * 12 + 4;
+                for (Map.Entry<String, ExifAttribute> entry : mExifData.getAttributes(
+                        ifdType).entrySet()) {
+                    // Convert tag name to tag number.
+                    final ExifTag tag = sExifTagMapsForWriting.get(ifdType).get(entry.getKey());
+                    final int tagNumber =
+                            Preconditions.checkNotNull(tag,
+                                    "Tag not supported: " + entry.getKey() + ". Tag needs to be "
+                                            + "ported from ExifInterface to ExifData.").number;
+                    final ExifAttribute attribute = entry.getValue();
+                    final int size = attribute.size();
+
+                    dataOutputStream.writeUnsignedShort(tagNumber);
+                    dataOutputStream.writeUnsignedShort(attribute.format);
+                    dataOutputStream.writeInt(attribute.numberOfComponents);
+                    if (size > 4) {
+                        dataOutputStream.writeUnsignedInt(dataOffset);
+                        dataOffset += size;
+                    } else {
+                        dataOutputStream.write(attribute.bytes);
+                        // Fill zero up to 4 bytes
+                        if (size < 4) {
+                            for (int i = size; i < 4; ++i) {
+                                dataOutputStream.writeByte(0);
+                            }
+                        }
+                    }
+                }
+
+                // Write the next offset. Since we aren't handling thumbnails, this is just 0.
+                dataOutputStream.writeUnsignedInt(0);
+
+                // Write values of data field exceeding 4 bytes after the next offset.
+                for (Map.Entry<String, ExifAttribute> entry : mExifData.getAttributes(
+                        ifdType).entrySet()) {
+                    ExifAttribute attribute = entry.getValue();
+
+                    if (attribute.bytes.length > 4) {
+                        dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length);
+                    }
+                }
+            }
+        }
+
+        // Reset the byte order to big endian in order to write remaining parts of the JPEG file.
+        dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+    }
+
+    static final class JpegHeader {
+        public static final short SOI =  (short) 0xFFD8;
+        public static final short APP1 = (short) 0xFFE1;
+        public static final short EOI = (short) 0xFFD9;
+
+        /**
+         *  SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT,
+         *  JPG, and DAC marker.
+         */
+        public static final short SOF0 = (short) 0xFFC0;
+        public static final short SOF15 = (short) 0xFFCF;
+        public static final short DHT = (short) 0xFFC4;
+        public static final short JPG = (short) 0xFFC8;
+        public static final short DAC = (short) 0xFFCC;
+
+        public static boolean isSofMarker(short marker) {
+            return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG
+                    && marker != DAC;
+        }
+
+        private JpegHeader() {}
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifTag.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifTag.java
new file mode 100644
index 0000000..c0df33b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifTag.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_DOUBLE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SINGLE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SLONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SSHORT;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_ULONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_UNDEFINED;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_USHORT;
+
+import androidx.exifinterface.media.ExifInterface;
+
+/**
+ * This class stores information of an EXIF tag. For more information about
+ * defined EXIF tags, please read the Jeita EXIF 2.2 standard.
+ *
+ * This class was pulled from the {@link ExifInterface} class.
+ *
+ * @see ExifInterface
+ */
+class ExifTag {
+    public final int number;
+    public final String name;
+    public final int primaryFormat;
+    public final int secondaryFormat;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ExifTag(String name, int number, int format) {
+        this.name = name;
+        this.number = number;
+        this.primaryFormat = format;
+        this.secondaryFormat = -1;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    ExifTag(String name, int number, int primaryFormat, int secondaryFormat) {
+        this.name = name;
+        this.number = number;
+        this.primaryFormat = primaryFormat;
+        this.secondaryFormat = secondaryFormat;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    boolean isFormatCompatible(int format) {
+        if (primaryFormat == IFD_FORMAT_UNDEFINED || format == IFD_FORMAT_UNDEFINED) {
+            return true;
+        } else if (primaryFormat == format || secondaryFormat == format) {
+            return true;
+        } else if ((primaryFormat == IFD_FORMAT_ULONG || secondaryFormat == IFD_FORMAT_ULONG)
+                && format == IFD_FORMAT_USHORT) {
+            return true;
+        } else if ((primaryFormat == IFD_FORMAT_SLONG || secondaryFormat == IFD_FORMAT_SLONG)
+                && format == IFD_FORMAT_SSHORT) {
+            return true;
+        } else return (primaryFormat == IFD_FORMAT_DOUBLE || secondaryFormat == IFD_FORMAT_DOUBLE)
+                && format == IFD_FORMAT_SINGLE;
+    }
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/LongRational.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/LongRational.java
new file mode 100644
index 0000000..3b6353d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/LongRational.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import androidx.annotation.NonNull;
+
+/**
+ * The rational data type of EXIF tag. Contains a pair of longs representing the
+ * numerator and denominator of a Rational number.
+ */
+final class LongRational {
+
+    private final long mNumerator;
+    private final long mDenominator;
+
+    /**
+     * Create a Rational with a given numerator and denominator.
+     */
+    LongRational(long nominator, long denominator) {
+        mNumerator = nominator;
+        mDenominator = denominator;
+    }
+
+    /**
+     * Creates a Rational from a double.
+     */
+    LongRational(double value) {
+        this((long) (value * 10000), 10000);
+    }
+
+    /**
+     * Gets the numerator of the rational.
+     */
+    long getNumerator() {
+        return mNumerator;
+    }
+
+    /**
+     * Gets the denominator of the rational
+     */
+    long getDenominator() {
+        return mDenominator;
+    }
+
+    /**
+     * Gets the rational value as type double. Will cause a divide-by-zero error
+     * if the denominator is 0.
+     */
+    double toDouble() {
+        return mNumerator / (double) mDenominator;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return mNumerator + "/" + mDenominator;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java
index e951da0..ea4f5a5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java
@@ -20,6 +20,7 @@
 import androidx.camera.core.ImageInfo;
 import androidx.camera.core.impl.CameraCaptureResult;
 import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
 
 /** An ImageInfo that is created by a {@link CameraCaptureResult}. */
 public final class CameraCaptureResultImageInfo implements ImageInfo {
@@ -46,6 +47,11 @@
         return 0;
     }
 
+    @Override
+    public void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+        mCameraCaptureResult.populateExifData(exifBuilder);
+    }
+
     @NonNull
     public CameraCaptureResult getCameraCaptureResult() {
         return mCameraCaptureResult;
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 8d0754a..2c16a41 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
@@ -344,7 +344,8 @@
                 ConfigPair configPair = configPairMap.get(useCase);
                 // Combine with default configuration.
                 UseCaseConfig<?> combinedUseCaseConfig =
-                        useCase.mergeConfigs(configPair.mExtendedConfig, configPair.mCameraConfig);
+                        useCase.mergeConfigs(cameraInfoInternal, configPair.mExtendedConfig,
+                                configPair.mCameraConfig);
                 configToUseCaseMap.put(combinedUseCaseConfig, useCase);
             }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java
new file mode 100644
index 0000000..773b1ad
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import android.media.ImageWriter;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Helper for accessing features of {@link ImageWriter} in a backwards compatible fashion.
+ */
+@RequiresApi(26)
+public final class ImageWriterCompat {
+
+    /**
+     * <p>
+     * Create a new ImageWriter with given number of max Images and format.
+     * </p>
+     * <p>
+     * The {@code maxImages} parameter determines the maximum number of
+     * {@link android.media.Image} objects that can be be dequeued from the
+     * {@code ImageWriter} simultaneously. Requesting more buffers will use up
+     * more memory, so it is important to use only the minimum number necessary.
+     * </p>
+     * <p>
+     * The format specifies the image format of this ImageWriter. The format
+     * from the {@code surface} will be overridden with this format. For example,
+     * if the surface is obtained from a {@link android.graphics.SurfaceTexture}, the default
+     * format may be {@link android.graphics.PixelFormat#RGBA_8888}. If the application creates an
+     * ImageWriter with this surface and {@link android.graphics.ImageFormat#PRIVATE}, this
+     * ImageWriter will be able to operate with {@link android.graphics.ImageFormat#PRIVATE} Images.
+     * </p>
+     * <p>
+     * Note that the consumer end-point may or may not be able to support Images with different
+     * format, for such case, the application should only use this method if the consumer is able
+     * to consume such images.
+     * </p>
+     * <p>
+     * The input Image size depends on the Surface that is provided by
+     * the downstream consumer end-point.
+     * </p>
+     *
+     * @param surface The destination Surface this writer produces Image data
+     *            into.
+     * @param maxImages The maximum number of Images the user will want to
+     *            access simultaneously for producing Image data. This should be
+     *            as small as possible to limit memory use. Once maxImages
+     *            Images are dequeued by the user, one of them has to be queued
+     *            back before a new Image can be dequeued for access via
+     *            {@link ImageWriter#dequeueInputImage()}.
+     * @param format The format of this ImageWriter. It can be any valid format specified by
+     *            {@link android.graphics.ImageFormat} or {@link android.graphics.PixelFormat}.
+     *
+     * @return a new ImageWriter instance.
+     */
+    @NonNull
+    public static ImageWriter newInstance(@NonNull Surface surface,
+            @IntRange(from = 1) int maxImages, int format) {
+        if (Build.VERSION.SDK_INT >= 26) {
+            return ImageWriterCompatApi26Impl.newInstance(surface, maxImages, format);
+        } else if (Build.VERSION.SDK_INT >= 29) {
+            return ImageWriterCompatApi29Impl.newInstance(surface, maxImages, format);
+        }
+
+        throw new RuntimeException(
+                "Unable to call newInstance(Surface, int, int) on API " + Build.VERSION.SDK_INT
+                        + ". Version 26 or higher required.");
+    }
+
+    // Class should not be instantiated.
+    private ImageWriterCompat() {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi26Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi26Impl.java
new file mode 100644
index 0000000..5fb2f66
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi26Impl.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import android.media.ImageWriter;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RequiresApi(26)
+final class ImageWriterCompatApi26Impl {
+    private static final String TAG = "ImageWriterCompatApi26";
+
+    private static Method sNewInstanceMethod;
+
+    static {
+        try {
+            sNewInstanceMethod = ImageWriter.class.getMethod("newInstance", Surface.class,
+                    int.class, int.class);
+        } catch (NoSuchMethodException e) {
+            Log.i(TAG, "Unable to initialize via reflection.", e);
+        }
+    }
+
+    @NonNull
+    static ImageWriter newInstance(@NonNull Surface surface, @IntRange(from = 1) int maxImages,
+            int format) {
+        Throwable t = null;
+        if (Build.VERSION.SDK_INT >= 26) {
+            try {
+                return (ImageWriter) Preconditions.checkNotNull(
+                        sNewInstanceMethod.invoke(null, surface, maxImages, format));
+            } catch (IllegalAccessException | InvocationTargetException e) {
+                t = e;
+            }
+        }
+
+        throw new RuntimeException("Unable to invoke newInstance(Surface, int, int) via "
+                + "reflection.", t);
+    }
+
+    // Class should not be instantiated.
+    private ImageWriterCompatApi26Impl() {
+    }
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi29Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi29Impl.java
new file mode 100644
index 0000000..a2ea19d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi29Impl.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import android.media.ImageWriter;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(29)
+final class ImageWriterCompatApi29Impl {
+
+    @NonNull
+    static ImageWriter newInstance(@NonNull Surface surface, @IntRange(from = 1) int maxImages,
+            int format) {
+        return ImageWriter.newInstance(surface, maxImages, format);
+    }
+
+    // Class should not be instantiated.
+    private ImageWriterCompatApi29Impl() {
+    }
+}
+
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/package-info.java
similarity index 79%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to camera/camera-core/src/main/java/androidx/camera/core/internal/compat/package-info.java
index f9cb2fe..439a386 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/package-info.java
@@ -14,7 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.core.internal.compat;
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java b/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java
index d82f3f7..656a101 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java
@@ -96,4 +96,13 @@
         final Integer minLoggingLevel = cameraXConfig.getMinimumLoggingLevel();
         assertThat(minLoggingLevel).isEqualTo(Logger.DEFAULT_MIN_LOG_LEVEL);
     }
+
+    @Test
+    public void canGetAvailableCamerasSelector() {
+        CameraSelector cameraSelector = new CameraSelector.Builder().build();
+        CameraXConfig cameraXConfig = new CameraXConfig.Builder()
+                .setAvailableCamerasLimiter(cameraSelector)
+                .build();
+        assertThat(cameraXConfig.getAvailableCamerasLimiter(null)).isEqualTo(cameraSelector);
+    }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 15de65f..7fa7f6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -104,7 +104,7 @@
 
         CameraInternal camera = new FakeCamera();
 
-        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2) -> {
+        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2, ignored3) -> {
             FakeCameraFactory cameraFactory = new FakeCameraFactory();
             cameraFactory.insertDefaultBackCamera(camera.getCameraInfoInternal().getCameraId(),
                     () -> camera);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index c085467..25a96c7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -30,7 +30,6 @@
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
 import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.SessionConfig
 import androidx.camera.core.impl.TagBundle
@@ -71,7 +70,6 @@
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicReference
-import kotlin.jvm.Throws
 
 private const val MAX_IMAGES = 3
 
@@ -104,7 +102,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _, _, _ ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
                     camera
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 3ee0eb8..986d317 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -23,6 +23,7 @@
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
+import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.SessionConfig
@@ -68,7 +69,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(
                     camera.cameraInfoInternal.cameraId
@@ -169,7 +170,7 @@
         cameraUseCaseAdapter!!.addUseCases(Collections.singleton<UseCase>(preview))
 
         // Set SurfaceProvider
-        var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+        var receivedTransformationInfo: TransformationInfo? = null
         preview.setSurfaceProvider { request ->
             request.setTransformationInfoListener(
                 CameraXExecutors.directExecutor(),
@@ -209,7 +210,7 @@
             ApplicationProvider.getApplicationContext(), TEST_CAMERA_SELECTOR
         )
         cameraUseCaseAdapter!!.addUseCases(Collections.singleton<UseCase>(preview))
-        var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+        var receivedTransformationInfo: TransformationInfo? = null
         preview.setSurfaceProvider { request ->
             request.setTransformationInfoListener(
                 CameraXExecutors.directExecutor(),
@@ -248,7 +249,7 @@
         // Get pending SurfaceRequest created by pipeline.
         val pendingSurfaceRequest = preview.mCurrentSurfaceRequest
         var receivedSurfaceRequest: SurfaceRequest? = null
-        var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+        var receivedTransformationInfo: TransformationInfo? = null
 
         // Act: set a SurfaceProvider after attachment.
         preview.setSurfaceProvider { request ->
@@ -323,13 +324,12 @@
         return bindToLifecycleAndGetResult(null).first
     }
 
-    private fun bindToLifecycleAndGetTransformationInfo(viewPort: ViewPort?):
-        SurfaceRequest.TransformationInfo {
-            return bindToLifecycleAndGetResult(viewPort).second
-        }
+    private fun bindToLifecycleAndGetTransformationInfo(viewPort: ViewPort?): TransformationInfo {
+        return bindToLifecycleAndGetResult(viewPort).second
+    }
 
     private fun bindToLifecycleAndGetResult(viewPort: ViewPort?): Pair<SurfaceRequest,
-        SurfaceRequest.TransformationInfo> {
+        TransformationInfo> {
         // Arrange.
         val sessionOptionUnpacker =
             { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
@@ -338,7 +338,7 @@
             .setSessionOptionUnpacker(sessionOptionUnpacker)
             .build()
         var surfaceRequest: SurfaceRequest? = null
-        var transformationInfo: SurfaceRequest.TransformationInfo? = null
+        var transformationInfo: TransformationInfo? = null
         preview.setSurfaceProvider { request ->
             request.setTransformationInfoListener(
                 CameraXExecutors.directExecutor(),
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
index 65074ab..2cc5e17 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
@@ -20,7 +20,6 @@
 import android.os.Build
 import android.os.Looper
 import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.testing.fakes.FakeCamera
@@ -52,7 +51,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _, _, _ ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
                     camera
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
index d880ad5..9b780d9 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
@@ -21,6 +21,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 public class QuirksTest {
@@ -41,7 +42,7 @@
     }
 
     @Test
-    public void returnNullForInexistentQuirk() {
+    public void returnNullForNonexistentQuirk() {
         final Quirk1 quirk1 = new Quirk1();
         final Quirk2 quirk2 = new Quirk2();
 
@@ -54,6 +55,52 @@
         assertThat(quirks.get(Quirk3.class)).isNull();
     }
 
+    @Test
+    public void containsReturnsTrueForExistentQuirk() {
+        final Quirk1 quirk1 = new Quirk1();
+
+        final List<Quirk> allQuirks = Collections.singletonList(quirk1);
+
+        final Quirks quirks = new Quirks(allQuirks);
+
+        assertThat(quirks.contains(Quirk1.class)).isTrue();
+    }
+
+    @Test
+    public void containsReturnsFalseForNonexistentQuirk() {
+        final Quirk1 quirk1 = new Quirk1();
+
+        final List<Quirk> allQuirks = Collections.singletonList(quirk1);
+
+        final Quirks quirks = new Quirks(allQuirks);
+
+        assertThat(quirks.contains(Quirk2.class)).isFalse();
+    }
+
+    @Test
+    public void containsReturnsTrueForExistentSuperInterfaceQuirk() {
+        final SubIQuirk subIQuirk = new SubIQuirk();
+
+        final List<Quirk> allQuirks = Collections.singletonList(subIQuirk);
+
+        final Quirks quirks = new Quirks(allQuirks);
+
+        assertThat(quirks.contains(SubIQuirk.class)).isTrue();
+        assertThat(quirks.contains(ISuperQuirk.class)).isTrue();
+    }
+
+    @Test
+    public void containsReturnsTrueForExistentSuperClassQuirk() {
+        final SubQuirk subQuirk = new SubQuirk();
+
+        final List<Quirk> allQuirks = Collections.singletonList(subQuirk);
+
+        final Quirks quirks = new Quirks(allQuirks);
+
+        assertThat(quirks.contains(SubQuirk.class)).isTrue();
+        assertThat(quirks.contains(SuperQuirk.class)).isTrue();
+    }
+
     static class Quirk1 implements Quirk {
     }
 
@@ -62,4 +109,17 @@
 
     static class Quirk3 implements Quirk {
     }
+
+    interface ISuperQuirk extends Quirk {
+    }
+
+    static class SuperQuirk implements Quirk {
+    }
+
+    static class SubQuirk extends SuperQuirk {
+    }
+
+    static class SubIQuirk implements ISuperQuirk {
+    }
+
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/ExifDataTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/ExifDataTest.kt
new file mode 100644
index 0000000..ac6541c
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/ExifDataTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils
+
+import android.os.Build
+import androidx.camera.core.impl.CameraCaptureMetaData
+import androidx.exifinterface.media.ExifInterface
+import androidx.exifinterface.media.ExifInterface.FLAG_FLASH_FIRED
+import androidx.exifinterface.media.ExifInterface.FLAG_FLASH_NO_FLASH_FUNCTION
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import java.util.concurrent.TimeUnit
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class ExifDataTest {
+
+    @Test
+    public fun canSetImageWidth() {
+        val exifData = ExifData.builderForDevice().setImageWidth(100).build()
+        assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).isEqualTo("100")
+    }
+
+    @Test
+    public fun canSetImageHeight() {
+        val exifData = ExifData.builderForDevice().setImageHeight(200).build()
+        assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).isEqualTo("200")
+    }
+
+    @Test
+    public fun canSetOrientationDegrees() {
+        val exifData0 = ExifData.builderForDevice().setOrientationDegrees(0).build()
+        val exifData90 = ExifData.builderForDevice().setOrientationDegrees(90).build()
+        val exifData180 = ExifData.builderForDevice().setOrientationDegrees(180).build()
+        val exifData270 = ExifData.builderForDevice().setOrientationDegrees(270).build()
+
+        assertThat(exifData0.getAttribute(ExifInterface.TAG_ORIENTATION))
+            .isEqualTo("${ExifInterface.ORIENTATION_NORMAL}")
+        assertThat(exifData90.getAttribute(ExifInterface.TAG_ORIENTATION))
+            .isEqualTo("${ExifInterface.ORIENTATION_ROTATE_90}")
+        assertThat(exifData180.getAttribute(ExifInterface.TAG_ORIENTATION))
+            .isEqualTo("${ExifInterface.ORIENTATION_ROTATE_180}")
+        assertThat(exifData270.getAttribute(ExifInterface.TAG_ORIENTATION))
+            .isEqualTo("${ExifInterface.ORIENTATION_ROTATE_270}")
+    }
+
+    @Test
+    public fun settingInvalidOrientationIsUndefined() {
+        // Only 0, 90, 180 and 270 are valid orientations. Use an invalid orientation.
+        val exifData = ExifData.builderForDevice().setOrientationDegrees(42).build()
+
+        assertThat(exifData.getAttribute(ExifInterface.TAG_ORIENTATION))
+            .isEqualTo("${ExifInterface.ORIENTATION_UNDEFINED}")
+    }
+
+    @Test
+    public fun canSetFlashState() {
+        val exifDataFired = ExifData.builderForDevice()
+            .setFlashState(CameraCaptureMetaData.FlashState.FIRED)
+            .build()
+        val exifDataReady = ExifData.builderForDevice()
+            .setFlashState(CameraCaptureMetaData.FlashState.READY)
+            .build()
+        val exifDataNone = ExifData.builderForDevice()
+            .setFlashState(CameraCaptureMetaData.FlashState.NONE)
+            .build()
+
+        // Unknown should not set the attribute
+        val exifDataUnknown = ExifData.builderForDevice()
+            .setFlashState(CameraCaptureMetaData.FlashState.UNKNOWN)
+            .build()
+
+        // Flash fired.
+        assertThat(exifDataFired.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+            .isEqualTo(FLAG_FLASH_FIRED)
+
+        // Has flash but not fired.
+        assertThat(exifDataReady.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+            .isEqualTo(0)
+
+        // No flash function.
+        assertThat(exifDataNone.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+            .isEqualTo(FLAG_FLASH_NO_FLASH_FUNCTION)
+
+        assertThat(exifDataUnknown.getAttribute(ExifInterface.TAG_FLASH)).isNull()
+    }
+
+    @Test
+    public fun canSetExposureTime() {
+        val exifData = ExifData.builderForDevice()
+            .setExposureTimeNanos(TimeUnit.SECONDS.toNanos(5))
+            .build()
+        assertThat(exifData.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)?.toFloat()?.toInt())
+            .isEqualTo(5)
+    }
+
+    @Test
+    public fun canSetLensFNumber() {
+        val exifData = ExifData.builderForDevice()
+            .setLensFNumber(1.2f)
+            .build()
+        assertThat(exifData.getAttribute(ExifInterface.TAG_F_NUMBER)).isEqualTo("1.2")
+    }
+
+    @Test
+    public fun canSetIso() {
+        val exifData = ExifData.builderForDevice()
+            .setIso(800)
+            .build()
+        assertThat(exifData.getAttribute(ExifInterface.TAG_SENSITIVITY_TYPE))
+            .isEqualTo("${ExifInterface.SENSITIVITY_TYPE_ISO_SPEED}")
+        assertThat(exifData.getAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY))
+            .isEqualTo("800")
+    }
+
+    @Test
+    public fun canSetFocalLength() {
+        val exifData = ExifData.builderForDevice()
+            .setFocalLength(5400f /*millimeters*/)
+            .build()
+        assertThat(
+            exifData.getAttribute(ExifInterface.TAG_FOCAL_LENGTH)
+                ?.split("/")
+                ?.map(String::toLong)
+                ?.reduce { numerator: Long, denominator: Long -> numerator / denominator }
+        ).isEqualTo(5400)
+    }
+
+    @Test
+    public fun canSetWhiteBalanceMode() {
+        val exifDataAuto = ExifData.builderForDevice()
+            .setWhiteBalanceMode(ExifData.WhiteBalanceMode.AUTO)
+            .build()
+        val exifDataManual = ExifData.builderForDevice()
+            .setWhiteBalanceMode(ExifData.WhiteBalanceMode.MANUAL)
+            .build()
+
+        assertThat(exifDataAuto.getAttribute(ExifInterface.TAG_WHITE_BALANCE)?.toShort())
+            .isEqualTo(ExifInterface.WHITE_BALANCE_AUTO)
+        assertThat(exifDataManual.getAttribute(ExifInterface.TAG_WHITE_BALANCE)?.toShort())
+            .isEqualTo(ExifInterface.WHITE_BALANCE_MANUAL)
+    }
+
+    @Test
+    public fun makeAndModelSetByDefaultBuilder() {
+        val exifDataDefault = ExifData.builderForDevice().build()
+
+        assertThat(exifDataDefault.getAttribute(ExifInterface.TAG_MAKE))
+            .isEqualTo(Build.MANUFACTURER)
+        assertThat(exifDataDefault.getAttribute(ExifInterface.TAG_MODEL))
+            .isEqualTo(Build.MODEL)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/ImageWriterCompatTest.java b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/ImageWriterCompatTest.java
new file mode 100644
index 0000000..e739bda
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/ImageWriterCompatTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.media.ImageWriter;
+import android.os.Build;
+import android.view.Surface;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.O)
+public final class ImageWriterCompatTest {
+
+    private static final int TEST_MAX_IMAGES = 4;
+    private static final int TEST_IMAGE_FORMAT = ImageFormat.YUV_420_888;
+    private Surface mTestSurface;
+    private SurfaceTexture mTestSurfaceTexture;
+
+    @Before
+    public void setUp() {
+        mTestSurfaceTexture = new SurfaceTexture(/* singleBufferMode= */ false);
+        mTestSurface = new Surface(mTestSurfaceTexture);
+    }
+
+    @After
+    public void tearDown() {
+        mTestSurface.release();
+        mTestSurfaceTexture.release();
+    }
+
+    @Test
+    public void canCreateNewInstance() {
+        ImageWriter imageWriter = ImageWriterCompat.newInstance(mTestSurface,
+                TEST_MAX_IMAGES, TEST_IMAGE_FORMAT);
+
+        assertThat(imageWriter).isNotNull();
+    }
+}
diff --git a/camera/camera-lifecycle/build.gradle b/camera/camera-lifecycle/build.gradle
index 57fe2aa..5b54bf6 100644
--- a/camera/camera-lifecycle/build.gradle
+++ b/camera/camera-lifecycle/build.gradle
@@ -44,7 +44,7 @@
     androidTestImplementation(project(":annotation:annotation-experimental"))
     androidTestImplementation(project(":concurrent:concurrent-futures-ktx"))
     androidTestImplementation(project(":internal-testutils-truth"))
-    androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.13.1")
+    androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.15.0")
 }
 
 android {
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index f38206c..a361f64 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -26,7 +26,6 @@
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.Preview
 import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
@@ -422,7 +421,7 @@
     @Test
     fun bindUseCases_withNotExistedLensFacingCamera() {
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _, _, _ ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertCamera(
                     CameraSelector.LENS_FACING_BACK,
@@ -441,7 +440,7 @@
 
         val appConfigBuilder = CameraXConfig.Builder()
             .setCameraFactoryProvider(cameraFactoryProvider)
-            .setDeviceSurfaceManagerProvider { _, _ -> FakeCameraDeviceSurfaceManager() }
+            .setDeviceSurfaceManagerProvider { _, _, _ -> FakeCameraDeviceSurfaceManager() }
             .setUseCaseConfigFactoryProvider { FakeUseCaseConfigFactory() }
 
         ProcessCameraProvider.configureInstance(appConfigBuilder.build())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java b/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java
index 951bd8f..23cffbc7 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 
@@ -32,12 +33,13 @@
     /** Return a map that associates UseCases to UseCaseConfigs with default settings. */
     @NonNull
     public static Map<UseCase, UseCaseConfig<?>> useCaseConfigMapWithDefaultSettingsFromUseCaseList(
-            @NonNull List<UseCase> useCases, @NonNull UseCaseConfigFactory useCaseConfigFactory) {
+            @NonNull CameraInfoInternal cameraInfo, @NonNull List<UseCase> useCases,
+            @NonNull UseCaseConfigFactory useCaseConfigFactory) {
         Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap = new HashMap<>();
 
         for (UseCase useCase : useCases) {
             // Combine with default configuration.
-            UseCaseConfig<?> combinedUseCaseConfig = useCase.mergeConfigs(null,
+            UseCaseConfig<?> combinedUseCaseConfig = useCase.mergeConfigs(cameraInfo, null,
                     useCase.getDefaultConfig(true, useCaseConfigFactory));
             useCaseToConfigMap.put(useCase, combinedUseCaseConfig);
         }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 8a551a6..9dd7dec 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -38,7 +38,7 @@
     /** Generates a fake {@link CameraXConfig}. */
     @NonNull
     public static CameraXConfig create() {
-        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2) -> {
+        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2, ignored3) -> {
             FakeCameraFactory cameraFactory = new FakeCameraFactory();
             cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0,
                     () -> new FakeCamera(CAMERA_ID_0, null,
@@ -52,7 +52,7 @@
         };
 
         CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
-                (context, cameraManager) -> new FakeCameraDeviceSurfaceManager();
+                (ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
 
         CameraXConfig.Builder appConfigBuilder =
                 new CameraXConfig.Builder()
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
index f020ad2..3a6ef21 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -31,8 +31,6 @@
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.LiveDataObservable;
 import androidx.camera.core.impl.Observable;
-import androidx.camera.core.impl.Quirk;
-import androidx.camera.core.impl.Quirks;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseAttachState;
 import androidx.camera.core.impl.utils.futures.Futures;
@@ -72,9 +70,6 @@
 
     private List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList();
 
-    @NonNull
-    private final List<Quirk> mCameraQuirks = Collections.emptyList();
-
     public FakeCamera() {
         this(DEFAULT_CAMERA_ID, /*cameraControl=*/null,
                 new FakeCameraInfoInternal(DEFAULT_CAMERA_ID));
@@ -300,17 +295,6 @@
         return mCameraInfoInternal;
     }
 
-    @NonNull
-    @Override
-    public Quirks getCameraQuirks() {
-        return new Quirks(mCameraQuirks);
-    }
-
-    /** Adds a quirk to the list of this camera's quirks. */
-    public void addCameraQuirk(@NonNull final Quirk quirk) {
-        mCameraQuirks.add(quirk);
-    }
-
     private void checkNotReleased() {
         if (mState == State.RELEASED) {
             throw new IllegalStateException("Camera has been released.");
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 19d3ab7..78aab0c 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -30,11 +30,15 @@
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.Quirks;
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.camera.core.internal.ImmutableZoomState;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -53,6 +57,9 @@
     private final MutableLiveData<ZoomState> mZoomLiveData;
     private String mImplementationType = IMPLEMENTATION_TYPE_FAKE;
 
+    @NonNull
+    private final List<Quirk> mCameraQuirks = new ArrayList<>();
+
     public FakeCameraInfoInternal() {
         this(/*sensorRotation=*/ 0, /*lensFacing=*/ CameraSelector.LENS_FACING_BACK);
     }
@@ -167,6 +174,17 @@
         throw new UnsupportedOperationException("Not Implemented");
     }
 
+    @NonNull
+    @Override
+    public Quirks getCameraQuirks() {
+        return new Quirks(mCameraQuirks);
+    }
+
+    /** Adds a quirk to the list of this camera's quirks. */
+    public void addCameraQuirk(@NonNull final Quirk quirk) {
+        mCameraQuirks.add(quirk);
+    }
+
     /**
      * Set the implementation type for testing
      */
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
index 6e97478..df8a0da 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
@@ -20,6 +20,7 @@
 import androidx.camera.core.ImageInfo;
 import androidx.camera.core.impl.MutableTagBundle;
 import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
 
 /**
  * A fake implementation of {@link ImageInfo} where the values are settable.
@@ -62,4 +63,9 @@
     public int getRotationDegrees() {
         return mRotationDegrees;
     }
+
+    @Override
+    public void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+        exifBuilder.setOrientationDegrees(mRotationDegrees);
+    }
 }
diff --git a/camera/camera-video/src/androidTest/AndroidManifest.xml b/camera/camera-video/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..9f27ec7
--- /dev/null
+++ b/camera/camera-video/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.video.test">
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+</manifest>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
new file mode 100644
index 0000000..db52f2a
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.video.internal
+
+import android.Manifest
+import android.media.AudioFormat
+import android.media.MediaRecorder
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.video.internal.encoder.FakeInputBuffer
+import androidx.camera.video.internal.encoder.noInvocation
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.rule.GrantPermissionRule
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import java.util.concurrent.Callable
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class AudioSourceTest {
+
+    companion object {
+        private const val SAMPLE_RATE = 8000
+        private const val DEFAULT_MIN_BUFFER_SIZE = 1024
+    }
+
+    @get:Rule
+    var mAudioPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        Manifest.permission.RECORD_AUDIO
+    )
+    private lateinit var audioSource: AudioSource
+    private lateinit var fakeBufferProvider: FakeBufferProvider
+    private val bufferFactoryInvocations = mock(Callable::class.java)
+
+    @Before
+    fun setUp() {
+        fakeBufferProvider = FakeBufferProvider {
+            bufferFactoryInvocations.call()
+            FakeInputBuffer()
+        }
+        fakeBufferProvider.setActive(true)
+
+        audioSource = AudioSource.Builder()
+            .setExecutor(CameraXExecutors.ioExecutor())
+            .setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
+            .setSampleRate(SAMPLE_RATE)
+            .setChannelConfig(AudioFormat.CHANNEL_IN_MONO)
+            .setAudioFormat(AudioFormat.ENCODING_PCM_16BIT)
+            .setDefaultBufferSize(DEFAULT_MIN_BUFFER_SIZE)
+            .setBufferProvider(fakeBufferProvider)
+            .build()
+    }
+
+    @After
+    fun tearDown() {
+        if (this::audioSource.isInitialized) {
+            audioSource.release()
+        }
+    }
+
+    @Test
+    fun canRestartAudioSource() {
+        for (i in 0..2) {
+            // Act.
+            audioSource.start()
+
+            // Assert.
+            // It should continuously send audio data by invoking BufferProvider#acquireBuffer
+            verify(bufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
+
+            // Act.
+            audioSource.stop()
+
+            // Assert.
+            verify(bufferFactoryInvocations, noInvocation(3000L, 6000L)).call()
+        }
+    }
+
+    @Test
+    fun bufferProviderStateChange_acquireBufferOrNot() {
+        // Arrange.
+        audioSource.start()
+
+        for (i in 0..2) {
+            // Act.
+            fakeBufferProvider.setActive(true)
+
+            // Assert.
+            // It should continuously send audio data by invoking BufferProvider#acquireBuffer
+            verify(bufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
+
+            // Act.
+            fakeBufferProvider.setActive(false)
+
+            // Assert.
+            verify(bufferFactoryInvocations, noInvocation(3000L, 6000L)).call()
+        }
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/FakeBufferProvider.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/FakeBufferProvider.kt
new file mode 100644
index 0000000..db8e4be
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/FakeBufferProvider.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.video.internal
+
+import androidx.annotation.GuardedBy
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.video.internal.encoder.InputBuffer
+import com.google.common.util.concurrent.ListenableFuture
+import java.lang.IllegalStateException
+import java.util.concurrent.Callable
+import java.util.concurrent.Executor
+
+class FakeBufferProvider(private val bufferFactory: Callable<InputBuffer>) :
+    BufferProvider<InputBuffer> {
+
+    private val lock = Object()
+    @GuardedBy("lock")
+    private val observers = mutableMapOf<Observable.Observer<BufferProvider.State>, Executor>()
+    @GuardedBy("lock")
+    private var state = BufferProvider.State.ACTIVE
+
+    override fun acquireBuffer(): ListenableFuture<InputBuffer> {
+        synchronized(lock) {
+            return if (state == BufferProvider.State.ACTIVE) {
+                Futures.immediateFuture(bufferFactory.call())
+            } else {
+                Futures.immediateFailedFuture(IllegalStateException("Not in ACTIVE state"))
+            }
+        }
+    }
+
+    override fun fetchData(): ListenableFuture<BufferProvider.State> {
+        synchronized(lock) {
+            return Futures.immediateFuture(state)
+        }
+    }
+
+    override fun addObserver(
+        executor: Executor,
+        observer: Observable.Observer<BufferProvider.State>
+    ) {
+        synchronized(observers) {
+            observers[observer] = executor
+        }
+        executor.execute { observer.onNewData(state) }
+    }
+
+    override fun removeObserver(observer: Observable.Observer<BufferProvider.State>) {
+        synchronized(lock) {
+            observers.remove(observer)
+        }
+    }
+
+    fun setActive(active: Boolean) {
+        val newState = if (active) BufferProvider.State.ACTIVE else BufferProvider.State.INACTIVE
+        val localObservers: Map<Observable.Observer<BufferProvider.State>, Executor>
+        synchronized(lock) {
+            if (state == newState) {
+                return
+            }
+            state = newState
+            localObservers = observers
+        }
+        for ((observer, executor) in localObservers) {
+            executor.execute { observer.onNewData(newState) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
index 64d1e7d..ffba5b9 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
@@ -16,12 +16,17 @@
 package androidx.camera.video.internal.encoder
 
 import android.media.AudioFormat
+import androidx.camera.core.impl.Observable.Observer
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.video.internal.BufferProvider
+import androidx.camera.video.internal.BufferProvider.State
+import androidx.concurrent.futures.await
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import kotlinx.coroutines.Dispatchers
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import org.junit.After
@@ -37,6 +42,10 @@
 import org.mockito.Mockito.verify
 import org.mockito.invocation.InvocationOnMock
 import java.nio.ByteBuffer
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -51,7 +60,7 @@
 
     private lateinit var encoder: Encoder
     private lateinit var encoderCallback: EncoderCallback
-    private lateinit var byteBufferProviderJob: Job
+    private lateinit var fakeAudioLoop: FakeAudioLoop
 
     @Before
     fun setup() {
@@ -74,25 +83,25 @@
         )
         encoder.setEncoderCallback(encoderCallback, CameraXExecutors.directExecutor())
 
-        // Prepare a fake audio source
-        val byteBuffer = ByteBuffer.allocateDirect(1024)
-        byteBufferProviderJob = GlobalScope.launch(Dispatchers.Default) {
-            while (true) {
-                byteBuffer.rewind()
-                (encoder.input as Encoder.ByteBufferInput).putByteBuffer(byteBuffer)
-                delay(200)
-            }
-        }
+        @Suppress("UNCHECKED_CAST")
+        fakeAudioLoop = FakeAudioLoop(encoder.input as BufferProvider<InputBuffer>)
     }
 
     @After
     fun tearDown() {
-        encoder.release()
-        byteBufferProviderJob.cancel(null)
+        if (this::encoder.isInitialized) {
+            encoder.release()
+        }
+        if (this::fakeAudioLoop.isInitialized) {
+            fakeAudioLoop.stop()
+        }
     }
 
     @Test
     fun discardInputBufferBeforeStart() {
+        // Arrange.
+        fakeAudioLoop.start()
+
         // Act.
         // Wait a second to receive data
         Thread.sleep(3000L)
@@ -103,6 +112,9 @@
 
     @Test
     fun canRestartEncoder() {
+        // Arrange.
+        fakeAudioLoop.start()
+
         for (i in 0..3) {
             // Arrange.
             clearInvocations(encoderCallback)
@@ -125,6 +137,9 @@
 
     @Test
     fun canRestartEncoderImmediately() {
+        // Arrange.
+        fakeAudioLoop.start()
+
         // Act.
         encoder.start()
         encoder.stop()
@@ -136,6 +151,9 @@
 
     @Test
     fun canPauseResumeEncoder() {
+        // Arrange.
+        fakeAudioLoop.start()
+
         // Act.
         encoder.start()
 
@@ -162,6 +180,9 @@
 
     @Test
     fun canPauseStopStartEncoder() {
+        // Arrange.
+        fakeAudioLoop.start()
+
         // Act.
         encoder.start()
 
@@ -191,4 +212,128 @@
         // Assert.
         verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
     }
+
+    @Test
+    fun bufferProvider_canAcquireBuffer() {
+        // Arrange.
+        encoder.start()
+
+        for (i in 0..8) {
+            // Act.
+            val inputBuffer = (encoder.input as Encoder.ByteBufferInput)
+                .acquireBuffer()
+                .get(3, TimeUnit.SECONDS)
+
+            // Assert.
+            assertThat(inputBuffer).isNotNull()
+            inputBuffer.cancel()
+        }
+    }
+
+    @Test
+    fun bufferProvider_canReceiveBufferProviderStateChange() {
+        // Arrange.
+        val stateRef = AtomicReference<State>()
+        val lock = Semaphore(0)
+        (encoder.input as Encoder.ByteBufferInput).addObserver(
+            CameraXExecutors.directExecutor(),
+            object : Observer<State> {
+                override fun onNewData(state: State?) {
+                    stateRef.set(state)
+                    lock.release()
+                }
+
+                override fun onError(t: Throwable) {
+                    stateRef.set(null)
+                    lock.release()
+                }
+            }
+        )
+
+        // Assert.
+        assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        assertThat(stateRef.get()).isEqualTo(State.INACTIVE)
+
+        // Act.
+        encoder.start()
+
+        // Assert.
+        assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        assertThat(stateRef.get()).isEqualTo(State.ACTIVE)
+
+        // Act.
+        encoder.pause()
+
+        // Assert
+        assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        assertThat(stateRef.get()).isEqualTo(State.INACTIVE)
+
+        // Act.
+        encoder.start()
+
+        // Assert.
+        assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        assertThat(stateRef.get()).isEqualTo(State.ACTIVE)
+
+        // Act.
+        encoder.stop()
+
+        // Assert.
+        assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+        assertThat(stateRef.get()).isEqualTo(State.INACTIVE)
+    }
+
+    private class FakeAudioLoop(private val bufferProvider: BufferProvider<InputBuffer>) {
+        private val inputByteBuffer = ByteBuffer.allocateDirect(1024)
+        private val started = AtomicBoolean(false)
+        private var job: Job? = null
+
+        fun start() {
+            if (started.getAndSet(true)) {
+                return
+            }
+            job = GlobalScope.launch(
+                CameraXExecutors.ioExecutor().asCoroutineDispatcher(),
+            ) {
+                while (true) {
+                    try {
+                        val inputBuffer = bufferProvider.acquireBuffer().await()
+                        inputBuffer.apply {
+                            byteBuffer.apply {
+                                put(
+                                    inputByteBuffer.apply {
+                                        clear()
+                                        limit(limit().coerceAtMost(byteBuffer.capacity()))
+                                    }
+                                )
+                                flip()
+                            }
+                            setPresentationTimeUs(System.nanoTime() / 1000L)
+                            submit()
+                        }
+                    } catch (e: IllegalStateException) {
+                        // For simplicity, AudioLoop doesn't monitor the encoder's state.
+                        // When an IllegalStateException is thrown by encoder which is not started,
+                        // AudioLoop should retry with a delay to avoid busy loop.
+                        // CancellationException is a subclass of IllegalStateException and is
+                        // ambiguous since the cancellation could be caused by ListenableFuture
+                        // was cancelled or coroutine Job was cancelled. For the
+                        // ListenableFuture case, AudioLoop will need to retry with a delay as
+                        // IllegalStateException. For the coroutine Job case, the loop should
+                        // be stopped. The goal can be simply achieved by calling delay() method
+                        // because the method will also get CancellationException if it is
+                        // coroutine Job cancellation, and eventually leave the audio loop.
+                        delay(300L)
+                    }
+                }
+            }
+        }
+
+        fun stop() {
+            if (!started.getAndSet(false)) {
+                return
+            }
+            job!!.cancel()
+        }
+    }
 }
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/FakeInputBuffer.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/FakeInputBuffer.kt
new file mode 100644
index 0000000..72fc1c4
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/FakeInputBuffer.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.video.internal.encoder
+
+import androidx.concurrent.futures.ResolvableFuture
+import com.google.common.util.concurrent.ListenableFuture
+import java.nio.ByteBuffer
+
+class FakeInputBuffer : InputBuffer {
+    private val byteBuffer = ByteBuffer.allocateDirect(1024)
+    private val terminationFuture = ResolvableFuture.create<Void>()
+
+    override fun getByteBuffer(): ByteBuffer {
+        throwIfTerminated()
+        return byteBuffer
+    }
+
+    override fun setPresentationTimeUs(presentationTimeUs: Long) {
+        throwIfTerminated()
+    }
+
+    override fun setEndOfStream(isEndOfStream: Boolean) {
+        throwIfTerminated()
+    }
+
+    override fun submit(): Boolean {
+        return terminationFuture.set(null)
+    }
+
+    override fun cancel(): Boolean {
+        return terminationFuture.set(null)
+    }
+
+    override fun getTerminationFuture(): ListenableFuture<Void> {
+        return terminationFuture
+    }
+
+    private fun throwIfTerminated() {
+        check(!terminationFuture.isDone)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
index 8e0613f..3b16b89 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
@@ -627,6 +627,7 @@
                 if (isCurrentCamera(cameraId)) {
                     // Only reset the pipeline when the bound camera is the same.
                     setupEncoder(cameraId, resolution);
+                    notifyReset();
                 }
             }
         });
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
new file mode 100644
index 0000000..5a08d74
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal;
+
+import static androidx.camera.video.internal.AudioSource.InternalState.CONFIGURED;
+import static androidx.camera.video.internal.AudioSource.InternalState.RELEASED;
+import static androidx.camera.video.internal.AudioSource.InternalState.STARTED;
+
+import android.annotation.SuppressLint;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.AudioTimestamp;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.annotation.ExecutedBy;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.video.internal.encoder.InputBuffer;
+import androidx.core.util.Preconditions;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+
+/**
+ * AudioSource is used to obtain audio raw data and write to the buffer from {@link BufferProvider}.
+ *
+ * <p>The audio raw data could be one of sources from the device. The target source can be
+ * specified with {@link Builder#setAudioSource(int)}.
+ *
+ * <p>Calling {@link #start} will start reading audio data from the target source and then write
+ * the data into the buffer from {@link BufferProvider}. Calling {@link #stop} will stop sending
+ * audio data. However, to really read/write data to buffer, the {@link BufferProvider}'s state
+ * must be {@link BufferProvider.State#ACTIVE}. So recording may temporarily pause when the
+ * {@link BufferProvider}'s state is {@link BufferProvider.State#INACTIVE}.
+ *
+ * @see BufferProvider
+ * @see AudioRecord
+ */
+public final class AudioSource {
+    private static final String TAG = "AudioSource";
+
+    enum InternalState {
+        /** The initial state or when {@link #stop} is called after started. */
+        CONFIGURED,
+
+        /** The state is when it is in {@link #CONFIGURED} state and {@link #start} is called. */
+        STARTED,
+
+        /** The state is when {@link #release} is called. */
+        RELEASED,
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final Executor mExecutor;
+
+    private final BufferProvider<InputBuffer> mBufferProvider;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final AudioRecord mAudioRecord;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final int mBufferSize;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    InternalState mState = CONFIGURED;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    BufferProvider.State mBufferProviderState = BufferProvider.State.INACTIVE;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    boolean mIsSendingAudio;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    AudioSource(@NonNull Executor executor,
+            @NonNull BufferProvider<InputBuffer> bufferProvider,
+            int audioSource,
+            int sampleRate,
+            int channelConfig,
+            int audioFormat,
+            int defaultBufferSize)
+            throws AudioSourceAccessException {
+        mExecutor = CameraXExecutors.newSequentialExecutor(Preconditions.checkNotNull(executor));
+        mBufferProvider = Preconditions.checkNotNull(bufferProvider);
+
+        int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
+        if (bufferSize <= 0) {
+            bufferSize = defaultBufferSize;
+        }
+        mBufferSize = bufferSize * 2;
+        try {
+            mAudioRecord = new AudioRecord(audioSource,
+                    sampleRate,
+                    channelConfig,
+                    audioFormat,
+                    mBufferSize);
+        } catch (IllegalArgumentException e) {
+            throw new AudioSourceAccessException("Unable to create AudioRecord", e);
+        }
+
+        if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
+            mAudioRecord.release();
+            throw new AudioSourceAccessException("Unable to initialize AudioRecord");
+        }
+
+        mBufferProvider.addObserver(mExecutor, mStateObserver);
+    }
+
+    /**
+     * Starts the AudioSource.
+     *
+     * <p>Audio data will start being sent to the {@link BufferProvider} when
+     * {@link BufferProvider}'s state is {@link BufferProvider.State#ACTIVE}.
+     *
+     * @throws IllegalStateException if the AudioSource is released.
+     */
+    public void start() {
+        mExecutor.execute(() -> {
+            switch (mState) {
+                case CONFIGURED:
+                    setState(STARTED);
+                    updateSendingAudio();
+                    break;
+                case STARTED:
+                    // Do nothing
+                    break;
+                case RELEASED:
+                    throw new IllegalStateException("AudioRecorder is released");
+            }
+        });
+    }
+
+    /**
+     * Stops the AudioSource.
+     *
+     * <p>Audio data will stop being sent to the {@link BufferProvider}.
+     *
+     * @throws IllegalStateException if it is released.
+     */
+    public void stop() {
+        mExecutor.execute(() -> {
+            switch (mState) {
+                case STARTED:
+                    setState(CONFIGURED);
+                    updateSendingAudio();
+                    break;
+                case CONFIGURED:
+                    // Do nothing
+                    break;
+                case RELEASED:
+                    throw new IllegalStateException("AudioRecorder is released");
+            }
+        });
+    }
+
+    /**
+     * Releases the AudioSource.
+     *
+     * <p>Once the AudioSource is released, it can not be used any more.
+     */
+    public void release() {
+        mExecutor.execute(() -> {
+            switch (mState) {
+                case STARTED:
+                case CONFIGURED:
+                    mBufferProvider.removeObserver(mStateObserver);
+                    mAudioRecord.release();
+                    stopSendingAudio();
+                    setState(RELEASED);
+                    break;
+                case RELEASED:
+                    // Do nothing
+                    break;
+            }
+        });
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mExecutor")
+    void updateSendingAudio() {
+        if (mState == STARTED && mBufferProviderState == BufferProvider.State.ACTIVE) {
+            startSendingAudio();
+        } else {
+            stopSendingAudio();
+        }
+    }
+
+    @ExecutedBy("mExecutor")
+    private void startSendingAudio() {
+        if (mIsSendingAudio) {
+            // Already started, ignore
+            return;
+        }
+        try {
+            Logger.d(TAG, "startSendingAudio");
+            mAudioRecord.startRecording();
+            if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
+                throw new IllegalStateException("Unable to start AudioRecord with state: "
+                                + mAudioRecord.getRecordingState());
+            }
+        } catch (IllegalStateException e) {
+            Logger.w(TAG, "Failed to start AudioRecord", e);
+            return;
+        }
+        mIsSendingAudio = true;
+        sendNextAudio();
+    }
+
+    @ExecutedBy("mExecutor")
+    private void stopSendingAudio() {
+        if (!mIsSendingAudio) {
+            // Already stopped, ignore.
+            return;
+        }
+        mIsSendingAudio = false;
+        try {
+            Logger.d(TAG, "stopSendingAudio");
+            mAudioRecord.stop();
+            if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
+                throw new IllegalStateException("Unable to stop AudioRecord with state: "
+                        + mAudioRecord.getRecordingState());
+            }
+        } catch (IllegalStateException e) {
+            Logger.w(TAG, "Failed to stop AudioRecord", e);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mExecutor")
+    void sendNextAudio() {
+        Futures.addCallback(mBufferProvider.acquireBuffer(), mAcquireBufferCallback, mExecutor);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mExecutor")
+    void setState(InternalState state) {
+        Logger.d(TAG, "Transitioning internal state: " + mState + " --> " + state);
+        mState = state;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressLint("UnsafeNewApiCall")
+    long generatePresentationTimeUs() {
+        long presentationTimeUs = -1;
+        if (Build.VERSION.SDK_INT >= 24) {
+            AudioTimestamp audioTimestamp = new AudioTimestamp();
+            if (mAudioRecord.getTimestamp(audioTimestamp, AudioTimestamp.TIMEBASE_MONOTONIC)
+                    == AudioRecord.SUCCESS) {
+                presentationTimeUs = audioTimestamp.nanoTime / 1000L;
+            } else {
+                Logger.w(TAG, "Unable to get audio timestamp");
+            }
+        }
+        if (presentationTimeUs == -1) {
+            presentationTimeUs = System.nanoTime() / 1000L;
+        }
+        return presentationTimeUs;
+    }
+
+    private final FutureCallback<InputBuffer> mAcquireBufferCallback =
+            new FutureCallback<InputBuffer>() {
+                @ExecutedBy("mExecutor")
+                @Override
+                public void onSuccess(InputBuffer inputBuffer) {
+                    if (!mIsSendingAudio) {
+                        inputBuffer.cancel();
+                        return;
+                    }
+                    ByteBuffer byteBuffer = inputBuffer.getByteBuffer();
+
+                    int length = mAudioRecord.read(byteBuffer, mBufferSize);
+                    if (length > 0) {
+                        byteBuffer.limit(length);
+                        inputBuffer.setPresentationTimeUs(generatePresentationTimeUs());
+                        inputBuffer.submit();
+                    } else {
+                        Logger.w(TAG, "Unable to read data from AudioRecord.");
+                        inputBuffer.cancel();
+                    }
+                    sendNextAudio();
+                }
+
+                @ExecutedBy("mExecutor")
+                @Override
+                public void onFailure(Throwable throwable) {
+                    Logger.d(TAG, "Unable to get input buffer, the BufferProvider "
+                            + "could be transitioning to INACTIVE state.");
+                }
+            };
+
+    private final Observable.Observer<BufferProvider.State> mStateObserver =
+            new Observable.Observer<BufferProvider.State>() {
+                @ExecutedBy("mExecutor")
+                @Override
+                public void onNewData(@Nullable BufferProvider.State state) {
+                    Logger.d(TAG, "Receive BufferProvider state change: "
+                            + mBufferProviderState + " to " + state);
+                    mBufferProviderState = state;
+                    updateSendingAudio();
+                }
+
+                @ExecutedBy("mExecutor")
+                @Override
+                public void onError(@NonNull Throwable t) {
+                    // Not define, should not be possible.
+                }
+            };
+
+    /**
+     * The builder of the AudioSource.
+     */
+    public static class Builder {
+        private Executor mExecutor;
+        private int mAudioSource;
+        private int mSampleRate;
+        private int mChannelConfig;
+        private int mAudioFormat;
+        private int mDefaultBufferSize;
+        private BufferProvider<InputBuffer> mBufferProvider;
+
+        /** Sets the executor to run the background task. */
+        @NonNull
+        public Builder setExecutor(@NonNull Executor executor) {
+            mExecutor = executor;
+            return this;
+        }
+
+        /**
+         * Sets the device audio source.
+         *
+         * @see android.media.MediaRecorder.AudioSource#MIC
+         * @see android.media.MediaRecorder.AudioSource#CAMCORDER
+         */
+        @NonNull
+        public Builder setAudioSource(int audioSource) {
+            mAudioSource = audioSource;
+            return this;
+        }
+
+        /** Sets the audio sample rate. */
+        @NonNull
+        public Builder setSampleRate(int sampleRate) {
+            mSampleRate = sampleRate;
+            return this;
+        }
+
+        /**
+         * Sets the channel config.
+         *
+         * @see AudioFormat#CHANNEL_IN_MONO
+         * @see AudioFormat#CHANNEL_IN_STEREO
+         */
+        @NonNull
+        public Builder setChannelConfig(int channelConfig) {
+            mChannelConfig = channelConfig;
+            return this;
+        }
+
+        /**
+         * Sets the audio format.
+         *
+         * @see AudioFormat#ENCODING_PCM_8BIT
+         * @see AudioFormat#ENCODING_PCM_16BIT
+         * @see AudioFormat#ENCODING_PCM_FLOAT
+         */
+        @NonNull
+        public Builder setAudioFormat(int audioFormat) {
+            mAudioFormat = audioFormat;
+            return this;
+        }
+
+        /**
+         * Sets the default buffer size.
+         *
+         * <p>AudioSource will try to generate a buffer size. But if it is unable to get one,
+         * it will apply this default buffer size.
+         */
+        @NonNull
+        public Builder setDefaultBufferSize(int bufferSize) {
+            mDefaultBufferSize = bufferSize;
+            return this;
+        }
+
+        /** Sets the {@link BufferProvider}. */
+        @NonNull
+        public Builder setBufferProvider(@NonNull BufferProvider<InputBuffer> bufferProvider) {
+            mBufferProvider = bufferProvider;
+            return this;
+        }
+
+        /** Build the AudioSource. */
+        @NonNull
+        public AudioSource build() throws AudioSourceAccessException {
+            return new AudioSource(mExecutor,
+                    mBufferProvider,
+                    mAudioSource,
+                    mSampleRate,
+                    mChannelConfig,
+                    mAudioFormat,
+                    mDefaultBufferSize
+            );
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/MalformedVersionException.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSourceAccessException.java
similarity index 63%
rename from car/app/app/src/main/java/androidx/car/app/MalformedVersionException.java
rename to camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSourceAccessException.java
index b685ecb..204ba20 100644
--- a/car/app/app/src/main/java/androidx/car/app/MalformedVersionException.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSourceAccessException.java
@@ -14,24 +14,22 @@
  * limitations under the License.
  */
 
-package androidx.car.app;
+package androidx.camera.video.internal;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-/**
- * An exception for malformed {@link CarAppVersion} strings.
- */
-public class MalformedVersionException extends Exception {
-    public MalformedVersionException(@Nullable String message) {
+/** An exception thrown to indicate an error has occurred during configuring an audio source. */
+public class AudioSourceAccessException extends Exception {
+
+    public AudioSourceAccessException(@Nullable String message) {
         super(message);
     }
 
-    public MalformedVersionException(@NonNull String message, @NonNull Throwable cause) {
+    public AudioSourceAccessException(@Nullable String message, @Nullable Throwable cause) {
         super(message, cause);
     }
 
-    public MalformedVersionException(@Nullable Throwable cause) {
+    public AudioSourceAccessException(@Nullable Throwable cause) {
         super(cause);
     }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/BufferProvider.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/BufferProvider.java
new file mode 100644
index 0000000..1dfd4c1
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/BufferProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Observable;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * BufferProvider provides buffers for writing data.
+ *
+ * <p>BufferProvider has {@link State}, it could be either {@link State#ACTIVE} or
+ * {@link State#INACTIVE}. The state can be fetched directly through {@link #fetchData()} or use
+ * {@link #addObserver} to receive state changes.
+ *
+ * <p>A buffer for writing data can be acquired with {@link #acquireBuffer()}". The buffer can
+ * only be obtained when the state is {@link State#ACTIVE}. If the state is
+ * {@link State#INACTIVE}, the {@link #acquireBuffer()} will return a failed
+ * {@link ListenableFuture} with {@link IllegalStateException}. If the state is transitioned from
+ * {@link State#ACTIVE} to {@link State#INACTIVE}, the incomplete {@link ListenableFuture} will
+ * get {@link java.util.concurrent.CancellationException}. Buffer acquisition can be cancelled
+ * with {@link ListenableFuture#cancel} if acquisition is not yet complete.
+ *
+ * @param <T> the buffer data type
+ */
+public interface BufferProvider<T> extends Observable<BufferProvider.State> {
+
+    /**
+     * Acquires a buffer.
+     *
+     * <p>A buffer can only be obtained when the state is {@link State#ACTIVE}. If the state is
+     * {@link State#INACTIVE}, the {@link #acquireBuffer()} will return an failed
+     * {@link ListenableFuture} with {@link IllegalStateException}. If the state is transitioned
+     * from {@link State#ACTIVE} to {@link State#INACTIVE}, the incomplete
+     * {@link ListenableFuture} will get {@link java.util.concurrent.CancellationException}.
+     * Buffer acquisition can be cancelled with {@link ListenableFuture#cancel} if acquisition
+     * is not yet complete.
+     *
+     * @return a {@link ListenableFuture} to represent the acquisition.
+     */
+    @NonNull
+    ListenableFuture<T> acquireBuffer();
+
+    /** The state of the BufferProvider. */
+    enum State {
+
+        /** The state means it is able to acquire a buffer. */
+        ACTIVE,
+
+        /**
+         * The state means it is not able to acquire buffer.
+         *
+         * <p>The acquisition via {@link #acquireBuffer()} will get a result with
+         * {@link IllegalStateException}.
+         */
+        INACTIVE,
+    }
+}
+
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
index 723edce..1cef3f9 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
@@ -88,7 +88,7 @@
         }
         try {
             mMediaCodec.releaseOutputBuffer(mBufferIndex, false);
-        } catch (MediaCodec.CodecException e) {
+        } catch (IllegalStateException e) {
             mClosedCompleter.setException(e);
             return;
         }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
index da6ebce..d6fa8a5 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
@@ -19,8 +19,8 @@
 import android.view.Surface;
 
 import androidx.annotation.NonNull;
+import androidx.camera.video.internal.BufferProvider;
 
-import java.nio.ByteBuffer;
 import java.util.concurrent.Executor;
 
 /**
@@ -108,18 +108,13 @@
         }
     }
 
-    /** A ByteBufferInput provides {@link #putByteBuffer} method to send raw data. */
-    interface ByteBufferInput extends EncoderInput {
-
-        /**
-         * Puts an input raw {@link ByteBuffer} to the encoder.
-         *
-         * <p>The input {@code ByteBuffer} must be put when encoder is in started and not paused
-         * state, otherwise the {@code ByteBuffer} will be dropped directly. Then the encoded data
-         * will be sent via {@link EncoderCallback#onEncodedData} callback.
-         *
-         * @param byteBuffer the input byte buffer
-         */
-        void putByteBuffer(@NonNull ByteBuffer byteBuffer);
+    /**
+     * A ByteBufferInput is a {@link BufferProvider} implementation and provides
+     * {@link InputBuffer} to write input data to the encoder.
+     *
+     * @see BufferProvider
+     * @see InputBuffer
+     */
+    interface ByteBufferInput extends EncoderInput, BufferProvider<InputBuffer> {
     }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index 00280df..acd7535 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -17,6 +17,7 @@
 package androidx.camera.video.internal.encoder;
 
 import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.CONFIGURED;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.ERROR;
 import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PAUSED;
 import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PENDING_RELEASE;
 import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PENDING_START;
@@ -36,24 +37,29 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.annotation.ExecutedBy;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Consumer;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.io.IOException;
-import java.nio.ByteBuffer;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * The encoder implementation.
@@ -106,20 +112,34 @@
          */
         PENDING_RELEASE,
 
+        /**
+         * Then state is when the encoder encounter error. Error state is a transitional state
+         * where encoder user is supposed to wait for {@link EncoderCallback#onEncodeStop} or
+         * {@link EncoderCallback#onEncodeError}. Any method call during this state should be
+         * ignore except {@link #release}.
+         */
+        ERROR,
+
         /** The state is when the encoder is released. */
         RELEASED,
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final Object mLock = new Object();
+    private final InternalStateObservable mStateObservable = new InternalStateObservable();
     private final MediaFormat mMediaFormat;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @GuardedBy("mLock")
     final MediaCodec mMediaCodec;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final EncoderInput mEncoderInput;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    final Executor mExecutor;
+    final Executor mEncoderExecutor;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final Queue<Integer> mFreeInputBufferIndexQueue = new ArrayDeque<>();
+    private final Queue<Completer<InputBuffer>> mAcquisitionQueue = new ArrayDeque<>();
+    private final Set<InputBuffer> mInputBufferSet = new HashSet<>();
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    final Set<EncodedDataImpl> mEncodedDataSet = new HashSet<>();
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @GuardedBy("mLock")
@@ -128,7 +148,6 @@
     @GuardedBy("mLock")
     Executor mEncoderCallbackExecutor = CameraXExecutors.mainThreadExecutor();
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @GuardedBy("mLock")
     InternalState mState;
 
     /**
@@ -143,10 +162,26 @@
         Preconditions.checkNotNull(executor);
         Preconditions.checkNotNull(encoderConfig);
 
-        mExecutor = CameraXExecutors.newSequentialExecutor(executor);
+        mEncoderExecutor = CameraXExecutors.newSequentialExecutor(executor);
 
         if (encoderConfig instanceof AudioEncoderConfig) {
-            mEncoderInput = new ByteBufferInput();
+            ByteBufferInput byteBufferInput = new ByteBufferInput();
+            // State change will run on mEncoderExecutor, use direct executor to avoid delay.
+            mStateObservable.addObserver(CameraXExecutors.directExecutor(),
+                    new Observable.Observer<InternalState>() {
+                        @ExecutedBy("mEncoderExecutor")
+                        @Override
+                        public void onNewData(@Nullable InternalState value) {
+                            byteBufferInput.setActive(value == STARTED);
+                        }
+
+                        @ExecutedBy("mEncoderExecutor")
+                        @Override
+                        public void onError(@NonNull Throwable t) {
+                            // No defined. Ignore.
+                        }
+                    });
+            mEncoderInput = byteBufferInput;
         } else if (encoderConfig instanceof VideoEncoderConfig) {
             mEncoderInput = new SurfaceInput();
         } else {
@@ -171,18 +206,22 @@
         setState(CONFIGURED);
     }
 
-    @SuppressWarnings("GuardedBy")
-    // It complains SurfaceInput#resetSurface and ByteBufferInput#clearFreeBuffers don't hold mLock
-    @GuardedBy("mLock")
+    @ExecutedBy("mEncoderExecutor")
     private void reset() {
+        mFreeInputBufferIndexQueue.clear();
+
+        // Cancel incomplete acquisitions if exists.
+        for (Completer<InputBuffer> completer : mAcquisitionQueue) {
+            completer.setCancelled();
+        }
+        mAcquisitionQueue.clear();
+
         mMediaCodec.reset();
         mMediaCodec.setCallback(new MediaCodecCallback());
         mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 
         if (mEncoderInput instanceof SurfaceInput) {
             ((SurfaceInput) mEncoderInput).resetSurface();
-        } else if (mEncoderInput instanceof ByteBufferInput) {
-            ((ByteBufferInput) mEncoderInput).clearFreeBuffers();
         }
     }
 
@@ -204,7 +243,7 @@
      */
     @Override
     public void start() {
-        synchronized (mLock) {
+        mEncoderExecutor.execute(() -> {
             switch (mState) {
                 case CONFIGURED:
                     try {
@@ -222,6 +261,7 @@
                     setState(STARTED);
                     break;
                 case STARTED:
+                case ERROR:
                 case PENDING_START:
                     // Do nothing
                     break;
@@ -235,7 +275,7 @@
                 default:
                     throw new IllegalStateException("Unknown state: " + mState);
             }
-        }
+        });
     }
 
     /**
@@ -244,21 +284,26 @@
      * <p>It will trigger {@link EncoderCallback#onEncodeStop} after the last encoded data. It can
      * call {@link #start} to start again.
      */
-    @SuppressWarnings("GuardedBy")
-    // It complains ByteBufferInput#signalEndOfInputStream doesn't hold mLock
     @Override
     public void stop() {
-        synchronized (mLock) {
+        mEncoderExecutor.execute(() -> {
             switch (mState) {
                 case CONFIGURED:
                 case STOPPING:
+                case ERROR:
                     // Do nothing
                     break;
                 case STARTED:
                 case PAUSED:
                     setState(STOPPING);
                     if (mEncoderInput instanceof ByteBufferInput) {
-                        ((ByteBufferInput) mEncoderInput).signalEndOfInputStream();
+                        // Wait for all issued input buffer done to avoid input loss.
+                        List<ListenableFuture<Void>> futures = new ArrayList<>();
+                        for (InputBuffer inputBuffer : mInputBufferSet) {
+                            futures.add(inputBuffer.getTerminationFuture());
+                        }
+                        Futures.successfulAsList(futures).addListener(this::signalEndOfInputStream,
+                                mEncoderExecutor);
                     } else if (mEncoderInput instanceof SurfaceInput) {
                         try {
                             mMediaCodec.signalEndOfInputStream();
@@ -277,7 +322,7 @@
                 default:
                     throw new IllegalStateException("Unknown state: " + mState);
             }
-        }
+        });
     }
 
     /**
@@ -288,10 +333,11 @@
      */
     @Override
     public void pause() {
-        synchronized (mLock) {
+        mEncoderExecutor.execute(() -> {
             switch (mState) {
                 case CONFIGURED:
                 case PAUSED:
+                case ERROR:
                 case STOPPING:
                 case PENDING_START_PAUSED:
                     // Do nothing
@@ -311,7 +357,7 @@
                 default:
                     throw new IllegalStateException("Unknown state: " + mState);
             }
-        }
+        });
     }
 
     /**
@@ -324,11 +370,12 @@
      */
     @Override
     public void release() {
-        synchronized (mLock) {
+        mEncoderExecutor.execute(() -> {
             switch (mState) {
                 case CONFIGURED:
                 case STARTED:
                 case PAUSED:
+                case ERROR:
                     mMediaCodec.release();
                     setState(RELEASED);
                     break;
@@ -344,7 +391,7 @@
                 default:
                     throw new IllegalStateException("Unknown state: " + mState);
             }
-        }
+        });
     }
 
     /**
@@ -363,42 +410,142 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @ExecutedBy("mEncoderExecutor")
     private void setState(InternalState state) {
+        if (mState == state) {
+            return;
+        }
         Logger.d(TAG, "Transitioning encoder internal state: " + mState + " --> " + state);
         mState = state;
+        mStateObservable.notifyState();
     }
 
-    @GuardedBy("mLock")
+    @ExecutedBy("mEncoderExecutor")
     private void updatePauseToMediaCodec(boolean paused) {
         Bundle bundle = new Bundle();
         bundle.putBoolean(MediaCodec.PARAMETER_KEY_SUSPEND, paused);
         mMediaCodec.setParameters(bundle);
     }
 
+    @ExecutedBy("mEncoderExecutor")
+    private void signalEndOfInputStream() {
+        Futures.addCallback(acquireInputBuffer(),
+                new FutureCallback<InputBuffer>() {
+                    @Override
+                    public void onSuccess(InputBuffer inputBuffer) {
+                        inputBuffer.setPresentationTimeUs(generatePresentationTimeUs());
+                        inputBuffer.setEndOfStream(true);
+                        inputBuffer.submit();
+
+                        Futures.addCallback(inputBuffer.getTerminationFuture(),
+                                new FutureCallback<Void>() {
+                                    @ExecutedBy("mEncoderExecutor")
+                                    @Override
+                                    public void onSuccess(@Nullable Void result) {
+                                        // Do nothing.
+                                    }
+
+                                    @ExecutedBy("mEncoderExecutor")
+                                    @Override
+                                    public void onFailure(Throwable t) {
+                                        if (t instanceof MediaCodec.CodecException) {
+                                            handleEncodeError(
+                                                    (MediaCodec.CodecException) t);
+                                        } else {
+                                            handleEncodeError(EncodeException.ERROR_UNKNOWN,
+                                                    t.getMessage(), t);
+                                        }
+                                    }
+                                }, mEncoderExecutor);
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        handleEncodeError(EncodeException.ERROR_UNKNOWN,
+                                "Unable to acquire InputBuffer.", t);
+                    }
+                }, mEncoderExecutor);
+    }
+
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @GuardedBy("mLock")
+    @ExecutedBy("mEncoderExecutor")
     void handleEncodeError(@NonNull MediaCodec.CodecException e) {
         handleEncodeError(EncodeException.ERROR_CODEC, e.getMessage(), e);
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @GuardedBy("mLock")
+    @ExecutedBy("mEncoderExecutor")
     void handleEncodeError(@EncodeException.ErrorType int error, @Nullable String message,
             @Nullable Throwable throwable) {
-        EncoderCallback encoderCallback = mEncoderCallback;
-        try {
-            mEncoderCallbackExecutor.execute(() -> encoderCallback.onEncodeError(
-                    new EncodeException(error, message, throwable)));
-        } catch (RejectedExecutionException re) {
-            Logger.e(TAG, "Unable to post to the supplied executor.", re);
+        switch (mState) {
+            case CONFIGURED:
+                // Unable to start MediaCodec. This is a fatal error. Try to reset the encoder.
+                notifyError(error, message, throwable);
+                reset();
+                break;
+            case STARTED:
+            case PAUSED:
+            case STOPPING:
+            case PENDING_START_PAUSED:
+            case PENDING_START:
+            case PENDING_RELEASE:
+                setState(ERROR);
+                stopMediaCodec(() -> notifyError(error, message, throwable));
+                break;
+            case ERROR:
+                Logger.w(TAG, "Get more than one error: " + message + "(" + error + ")",
+                        throwable);
+                break;
+            case RELEASED:
+                // Do nothing
+                break;
         }
-        mMediaCodec.stop();
-        handleStopped();
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @GuardedBy("mLock")
+    void notifyError(@EncodeException.ErrorType int error, @Nullable String message,
+            @Nullable Throwable throwable) {
+        EncoderCallback callback;
+        Executor executor;
+        synchronized (mLock) {
+            callback = mEncoderCallback;
+            executor = mEncoderCallbackExecutor;
+        }
+        try {
+            executor.execute(
+                    () -> callback.onEncodeError(new EncodeException(error, message, throwable)));
+        } catch (RejectedExecutionException e) {
+            Logger.e(TAG, "Unable to post to the supplied executor.", e);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mEncoderExecutor")
+    void stopMediaCodec(@Nullable Runnable afterStop) {
+        /*
+         * MediaCodec#close will free all its input/output ByteBuffers. Therefore, before calling
+         * MediaCodec#close, it must ensure all dispatched EncodedData(output ByteBuffers) and
+         * InputBuffer(input ByteBuffers) are complete. Otherwise, the ByteBuffer receiver will
+         * get buffer overflow when accessing the ByteBuffers.
+         */
+        List<ListenableFuture<Void>> futures = new ArrayList<>();
+        for (EncodedDataImpl dataToClose : mEncodedDataSet) {
+            futures.add(dataToClose.getClosedFuture());
+        }
+        for (InputBuffer inputBuffer : mInputBufferSet) {
+            futures.add(inputBuffer.getTerminationFuture());
+        }
+        Futures.successfulAsList(futures).addListener(() -> {
+            mMediaCodec.stop();
+            if (afterStop != null) {
+                afterStop.run();
+            }
+            handleStopped();
+        }, mEncoderExecutor);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mEncoderExecutor")
     void handleStopped() {
         if (mState == PENDING_RELEASE) {
             mMediaCodec.release();
@@ -417,25 +564,115 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mEncoderExecutor")
+    @NonNull
+    ListenableFuture<InputBuffer> acquireInputBuffer() {
+        switch (mState) {
+            case CONFIGURED:
+                return Futures.immediateFailedFuture(new IllegalStateException(
+                        "Encoder is not started yet."));
+            case STARTED:
+            case PAUSED:
+            case STOPPING:
+            case PENDING_START:
+            case PENDING_START_PAUSED:
+            case PENDING_RELEASE:
+                AtomicReference<Completer<InputBuffer>> ref = new AtomicReference<>();
+                ListenableFuture<InputBuffer> future = CallbackToFutureAdapter.getFuture(
+                        completer -> {
+                            ref.set(completer);
+                            return "acquireInputBuffer";
+                        });
+                Completer<InputBuffer> completer = Preconditions.checkNotNull(ref.get());
+                mAcquisitionQueue.offer(completer);
+                completer.addCancellationListener(() -> mAcquisitionQueue.remove(completer),
+                        mEncoderExecutor);
+                matchAcquisitionsAndFreeBufferIndexes();
+                return future;
+            case ERROR:
+                return Futures.immediateFailedFuture(new IllegalStateException(
+                        "Encoder is in error state."));
+            case RELEASED:
+                return Futures.immediateFailedFuture(new IllegalStateException(
+                        "Encoder is released."));
+            default:
+                throw new IllegalStateException("Unknown state: " + mState);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mEncoderExecutor")
+    void matchAcquisitionsAndFreeBufferIndexes() {
+        while (!mAcquisitionQueue.isEmpty() && !mFreeInputBufferIndexQueue.isEmpty()) {
+            Completer<InputBuffer> completer = mAcquisitionQueue.poll();
+            int bufferIndex = mFreeInputBufferIndexQueue.poll();
+
+            InputBufferImpl inputBuffer;
+            try {
+                inputBuffer = new InputBufferImpl(mMediaCodec, bufferIndex);
+            } catch (MediaCodec.CodecException e) {
+                handleEncodeError(e);
+                return;
+            }
+            if (completer.set(inputBuffer)) {
+                mInputBufferSet.add(inputBuffer);
+                inputBuffer.getTerminationFuture().addListener(
+                        () -> mInputBufferSet.remove(inputBuffer), mEncoderExecutor);
+            } else {
+                inputBuffer.cancel();
+            }
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     static long generatePresentationTimeUs() {
         return System.nanoTime() / 1000L;
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    class InternalStateObservable implements Observable<InternalState> {
+
+        private final Map<Observer<InternalState>, Executor> mObservers = new LinkedHashMap<>();
+
+        @ExecutedBy("mEncoderExecutor")
+        void notifyState() {
+            final InternalState state = mState;
+            for (Map.Entry<Observer<InternalState>, Executor> entry : mObservers.entrySet()) {
+                entry.getValue().execute(() -> entry.getKey().onNewData(state));
+            }
+        }
+
+        @ExecutedBy("mEncoderExecutor")
+        @NonNull
+        @Override
+        public ListenableFuture<InternalState> fetchData() {
+            return Futures.immediateFuture(mState);
+        }
+
+        @ExecutedBy("mEncoderExecutor")
+        @Override
+        public void addObserver(@NonNull Executor executor,
+                @NonNull Observer<InternalState> observer) {
+            final InternalState state = mState;
+            mObservers.put(observer, executor);
+            executor.execute(() -> observer.onNewData(state));
+        }
+
+        @ExecutedBy("mEncoderExecutor")
+        @Override
+        public void removeObserver(@NonNull Observer<InternalState> observer) {
+            mObservers.remove(observer);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     class MediaCodecCallback extends MediaCodec.Callback {
 
-        @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-        @GuardedBy("mLock")
-        final Set<EncodedDataImpl> mEncodedDataSet = new HashSet<>();
-
-        @GuardedBy("mLock")
         private boolean mHasFirstData = false;
 
-        @SuppressWarnings("GuardedBy")
-        // It complains ByteBufferInput#putFreeBufferIndex doesn't hold mLock
         @Override
         public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
-            synchronized (mLock) {
+            mEncoderExecutor.execute(() -> {
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -443,24 +680,24 @@
                     case PENDING_START:
                     case PENDING_START_PAUSED:
                     case PENDING_RELEASE:
-                        if (mEncoderInput instanceof ByteBufferInput) {
-                            ((ByteBufferInput) mEncoderInput).putFreeBufferIndex(index);
-                        }
+                        mFreeInputBufferIndexQueue.offer(index);
+                        matchAcquisitionsAndFreeBufferIndexes();
                         break;
                     case CONFIGURED:
+                    case ERROR:
                     case RELEASED:
                         // Do nothing
                         break;
                     default:
                         throw new IllegalStateException("Unknown state: " + mState);
                 }
-            }
+            });
         }
 
         @Override
         public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
                 @NonNull MediaCodec.BufferInfo bufferInfo) {
-            synchronized (mLock) {
+            mEncoderExecutor.execute(() -> {
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -468,8 +705,12 @@
                     case PENDING_START:
                     case PENDING_START_PAUSED:
                     case PENDING_RELEASE:
-                        final EncoderCallback encoderCallback = mEncoderCallback;
-                        final Executor executor = mEncoderCallbackExecutor;
+                        final EncoderCallback encoderCallback;
+                        final Executor executor;
+                        synchronized (mLock) {
+                            encoderCallback = mEncoderCallback;
+                            executor = mEncoderCallbackExecutor;
+                        }
 
                         // Handle start of stream
                         if (!mHasFirstData) {
@@ -507,25 +748,21 @@
                                     new FutureCallback<Void>() {
                                         @Override
                                         public void onSuccess(@Nullable Void result) {
-                                            synchronized (mLock) {
-                                                mEncodedDataSet.remove(encodedData);
-                                            }
+                                            mEncodedDataSet.remove(encodedData);
                                         }
 
                                         @Override
                                         public void onFailure(Throwable t) {
-                                            synchronized (mLock) {
-                                                mEncodedDataSet.remove(encodedData);
-                                                if (t instanceof MediaCodec.CodecException) {
-                                                    handleEncodeError(
-                                                            (MediaCodec.CodecException) t);
-                                                } else {
-                                                    handleEncodeError(EncodeException.ERROR_UNKNOWN,
-                                                            t.getMessage(), t);
-                                                }
+                                            mEncodedDataSet.remove(encodedData);
+                                            if (t instanceof MediaCodec.CodecException) {
+                                                handleEncodeError(
+                                                        (MediaCodec.CodecException) t);
+                                            } else {
+                                                handleEncodeError(EncodeException.ERROR_UNKNOWN,
+                                                        t.getMessage(), t);
                                             }
                                         }
-                                    }, CameraXExecutors.directExecutor());
+                                    }, mEncoderExecutor);
                             try {
                                 executor.execute(() -> encoderCallback.onEncodedData(encodedData));
                             } catch (RejectedExecutionException e) {
@@ -543,56 +780,29 @@
 
                         // Handle end of stream
                         if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
-                            // Wait for all data closed
-                            List<ListenableFuture<Void>> waitForCloseFutures = new ArrayList<>();
-                            for (EncodedDataImpl dataToClose : mEncodedDataSet) {
-                                waitForCloseFutures.add(dataToClose.getClosedFuture());
-                            }
-                            Futures.addCallback(Futures.allAsList(waitForCloseFutures),
-                                    new FutureCallback<List<Void>>() {
-                                        @Override
-                                        public void onSuccess(@Nullable List<Void> result) {
-                                            synchronized (mLock) {
-                                                mMediaCodec.stop();
-                                                try {
-                                                    executor.execute(encoderCallback::onEncodeStop);
-                                                } catch (RejectedExecutionException e) {
-                                                    Logger.e(TAG,
-                                                            "Unable to post to the supplied "
-                                                                    + "executor.", e);
-                                                }
-                                                handleStopped();
-                                            }
-                                        }
-
-                                        @Override
-                                        public void onFailure(Throwable t) {
-                                            synchronized (mLock) {
-                                                if (t instanceof MediaCodec.CodecException) {
-                                                    handleEncodeError(
-                                                            (MediaCodec.CodecException) t);
-                                                } else {
-                                                    handleEncodeError(EncodeException.ERROR_UNKNOWN,
-                                                            t.getMessage(), t);
-                                                }
-                                            }
-                                        }
-                                    }, CameraXExecutors.directExecutor());
+                            stopMediaCodec(() -> {
+                                try {
+                                    executor.execute(encoderCallback::onEncodeStop);
+                                } catch (RejectedExecutionException e) {
+                                    Logger.e(TAG, "Unable to post to the supplied executor.", e);
+                                }
+                            });
                         }
                         break;
                     case CONFIGURED:
+                    case ERROR:
                     case RELEASED:
                         // Do nothing
                         break;
                     default:
                         throw new IllegalStateException("Unknown state: " + mState);
                 }
-            }
+            });
         }
 
         @Override
         public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
-            synchronized (mLock) {
+            mEncoderExecutor.execute(() -> {
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -603,19 +813,20 @@
                         handleEncodeError(e);
                         break;
                     case CONFIGURED:
+                    case ERROR:
                     case RELEASED:
                         // Do nothing
                         break;
                     default:
                         throw new IllegalStateException("Unknown state: " + mState);
                 }
-            }
+            });
         }
 
         @Override
         public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
                 @NonNull MediaFormat mediaFormat) {
-            synchronized (mLock) {
+            mEncoderExecutor.execute(() -> {
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -623,28 +834,36 @@
                     case PENDING_START:
                     case PENDING_START_PAUSED:
                     case PENDING_RELEASE:
-                        EncoderCallback encoderCallback = mEncoderCallback;
+                        EncoderCallback encoderCallback;
+                        Executor executor;
+                        synchronized (mLock) {
+                            encoderCallback = mEncoderCallback;
+                            executor = mEncoderCallbackExecutor;
+                        }
                         try {
-                            mEncoderCallbackExecutor.execute(
+                            executor.execute(
                                     () -> encoderCallback.onOutputConfigUpdate(() -> mediaFormat));
                         } catch (RejectedExecutionException e) {
                             Logger.e(TAG, "Unable to post to the supplied executor.", e);
                         }
                         break;
                     case CONFIGURED:
+                    case ERROR:
                     case RELEASED:
                         // Do nothing
                         break;
                     default:
                         throw new IllegalStateException("Unknown state: " + mState);
                 }
-            }
+            });
         }
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     class SurfaceInput implements Encoder.SurfaceInput {
 
+        private final Object mLock = new Object();
+
         @GuardedBy("mLock")
         private Surface mSurface;
 
@@ -663,42 +882,50 @@
         @Override
         public void setOnSurfaceUpdateListener(@NonNull Executor executor,
                 @NonNull OnSurfaceUpdateListener listener) {
+            Surface surface;
             synchronized (mLock) {
                 mSurfaceUpdateListener = Preconditions.checkNotNull(listener);
                 mSurfaceUpdateExecutor = Preconditions.checkNotNull(executor);
-
-                if (mSurface != null) {
-                    notifySurfaceUpdate(mSurface);
-                }
-
+                surface = mSurface;
+            }
+            if (surface != null) {
+                notifySurfaceUpdate(executor, listener, surface);
             }
         }
 
-        @GuardedBy("mLock")
         @SuppressLint("UnsafeNewApiCall")
         void resetSurface() {
-            if (Build.VERSION.SDK_INT >= 23) {
-                if (mSurface == null) {
-                    mSurface = MediaCodec.createPersistentInputSurface();
-                    notifySurfaceUpdate(mSurface);
+            Surface surface;
+            Executor executor;
+            OnSurfaceUpdateListener listener;
+            synchronized (mLock) {
+                if (Build.VERSION.SDK_INT >= 23) {
+                    if (mSurface == null) {
+                        mSurface = MediaCodec.createPersistentInputSurface();
+                        surface = mSurface;
+                    } else {
+                        surface = null;
+                    }
+                    mMediaCodec.setInputSurface(mSurface);
+                } else {
+                    mSurface = mMediaCodec.createInputSurface();
+                    surface = mSurface;
                 }
-                mMediaCodec.setInputSurface(mSurface);
-            } else {
-                mSurface = mMediaCodec.createInputSurface();
-                notifySurfaceUpdate(mSurface);
+                listener = mSurfaceUpdateListener;
+                executor = mSurfaceUpdateExecutor;
+            }
+            if (surface != null && listener != null && executor != null) {
+                notifySurfaceUpdate(executor, listener, surface);
             }
         }
 
-        @GuardedBy("mLock")
-        private void notifySurfaceUpdate(@NonNull Surface surface) {
-            if (mSurfaceUpdateListener != null && mSurfaceUpdateExecutor != null) {
-                OnSurfaceUpdateListener listener = mSurfaceUpdateListener;
-                try {
-                    mSurfaceUpdateExecutor.execute(() -> listener.onSurfaceUpdate(surface));
-                } catch (RejectedExecutionException e) {
-                    Logger.e(TAG, "Unable to post to the supplied executor.", e);
-                    surface.release();
-                }
+        private void notifySurfaceUpdate(@NonNull Executor executor,
+                @NonNull OnSurfaceUpdateListener listener, @NonNull Surface surface) {
+            try {
+                executor.execute(() -> listener.onSurfaceUpdate(surface));
+            } catch (RejectedExecutionException e) {
+                Logger.e(TAG, "Unable to post to the supplied executor.", e);
+                surface.release();
             }
         }
     }
@@ -706,139 +933,91 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     class ByteBufferInput implements Encoder.ByteBufferInput {
 
-        @GuardedBy("mLock")
-        private final Queue<Consumer<Integer>> mListenerQueue = new ArrayDeque<>();
+        private final Map<Observer<State>, Executor> mStateObservers = new LinkedHashMap<>();
 
-        @GuardedBy("mLock")
-        private final Queue<Integer> mFreeBufferIndexQueue = new ArrayDeque<>();
+        private State mBufferProviderState = State.INACTIVE;
+
+        private final List<ListenableFuture<InputBuffer>> mAcquisitionList = new ArrayList<>();
 
         /** {@inheritDoc} */
+        @NonNull
         @Override
-        public void putByteBuffer(@NonNull ByteBuffer byteBuffer) {
-            synchronized (mLock) {
-                switch (mState) {
-                    case STARTED:
-                        // Here it means the byteBuffer should definitely be queued into codec.
-                        acquireFreeBufferIndex(freeBufferIndex -> {
-                            ByteBuffer inputBuffer = null;
-                            synchronized (mLock) {
-                                if (mState == STARTED
-                                        || mState == PAUSED
-                                        || mState == STOPPING
-                                        || mState == PENDING_START
-                                        || mState == PENDING_START_PAUSED
-                                        || mState == PENDING_RELEASE) {
-                                    try {
-                                        inputBuffer = mMediaCodec.getInputBuffer(freeBufferIndex);
-                                    } catch (MediaCodec.CodecException e) {
-                                        handleEncodeError(e);
-                                        return;
-                                    }
-                                }
-                            }
-
-                            if (inputBuffer == null) {
-                                return;
-                            }
-                            inputBuffer.put(byteBuffer);
-
-                            synchronized (mLock) {
-                                if (mState == STARTED
-                                        || mState == PAUSED
-                                        || mState == STOPPING
-                                        || mState == PENDING_START
-                                        || mState == PENDING_START_PAUSED
-                                        || mState == PENDING_RELEASE) {
-                                    try {
-                                        mMediaCodec.queueInputBuffer(freeBufferIndex, 0,
-                                                inputBuffer.position(),
-                                                generatePresentationTimeUs(),
-                                                0);
-                                    } catch (MediaCodec.CodecException e) {
-                                        handleEncodeError(e);
-                                        return;
-                                    }
-                                }
-                            }
-                        });
-                        break;
-                    case PAUSED:
-                        // Drop the data
-                        break;
-                    case CONFIGURED:
-                    case STOPPING:
-                    case PENDING_START:
-                    case PENDING_RELEASE:
-                    case RELEASED:
-                        // Do nothing
-                        break;
-                    default:
-                        throw new IllegalStateException("Unknown state: " + mState);
-                }
-            }
-        }
-
-        @GuardedBy("mLock")
-        void signalEndOfInputStream() {
-            acquireFreeBufferIndex(freeBufferIndex -> {
-                synchronized (mLock) {
-                    switch (mState) {
-                        case STARTED:
-                        case PAUSED:
-                        case STOPPING:
-                        case PENDING_START:
-                        case PENDING_START_PAUSED:
-                        case PENDING_RELEASE:
-                            try {
-                                mMediaCodec.queueInputBuffer(freeBufferIndex, 0, 0,
-                                        generatePresentationTimeUs(),
-                                        MediaCodec.BUFFER_FLAG_END_OF_STREAM);
-                            } catch (MediaCodec.CodecException e) {
-                                handleEncodeError(e);
-                            }
-                            break;
-                        case CONFIGURED:
-                        case RELEASED:
-                            // Do nothing
-                            break;
-                        default:
-                            throw new IllegalStateException("Unknown state: " + mState);
-                    }
-                }
+        public ListenableFuture<State> fetchData() {
+            return CallbackToFutureAdapter.getFuture(completer -> {
+                mEncoderExecutor.execute(() -> completer.set(mBufferProviderState));
+                return "fetchData";
             });
         }
 
-        @GuardedBy("mLock")
-        void putFreeBufferIndex(int index) {
-            mFreeBufferIndexQueue.offer(index);
-            match();
+        /** {@inheritDoc} */
+        @NonNull
+        @Override
+        public ListenableFuture<InputBuffer> acquireBuffer() {
+            return CallbackToFutureAdapter.getFuture(completer -> {
+                mEncoderExecutor.execute(() -> {
+                    if (mBufferProviderState == State.ACTIVE) {
+                        ListenableFuture<InputBuffer> future = acquireInputBuffer();
+                        Futures.propagate(future, completer);
+                        // Cancel by outer, also cancel internal future.
+                        completer.addCancellationListener(() -> future.cancel(true),
+                                CameraXExecutors.directExecutor());
+
+                        // Keep tracking the acquisition by internal future. Once the provider state
+                        // transition to inactive, cancel the internal future can also send signal
+                        // to outer future since we propagate the internal result to the completer.
+                        mAcquisitionList.add(future);
+                        future.addListener(() -> mAcquisitionList.remove(future), mEncoderExecutor);
+                    } else if (mBufferProviderState == State.INACTIVE) {
+                        completer.setException(
+                                new IllegalStateException("BufferProvider is not active."));
+                    } else {
+                        completer.setException(
+                                new IllegalStateException(
+                                        "Unknown state: " + mBufferProviderState));
+                    }
+                });
+                return "acquireBuffer";
+            });
         }
 
-        @GuardedBy("mLock")
-        void clearFreeBuffers() {
-            mListenerQueue.clear();
-            mFreeBufferIndexQueue.clear();
+        /** {@inheritDoc} */
+        @Override
+        public void addObserver(@NonNull Executor executor, @NonNull Observer<State> observer) {
+            mEncoderExecutor.execute(() -> {
+                mStateObservers.put(Preconditions.checkNotNull(observer),
+                        Preconditions.checkNotNull(executor));
+                final State state = mBufferProviderState;
+                executor.execute(() -> observer.onNewData(state));
+            });
         }
 
-        @GuardedBy("mLock")
-        private void acquireFreeBufferIndex(
-                @NonNull Consumer<Integer> onFreeBufferIndexListener) {
-            synchronized (mLock) {
-                mListenerQueue.offer(onFreeBufferIndexListener);
-                match();
+        /** {@inheritDoc} */
+        @Override
+        public void removeObserver(@NonNull Observer<State> observer) {
+            mEncoderExecutor.execute(
+                    () -> mStateObservers.remove(Preconditions.checkNotNull(observer)));
+        }
+
+        @ExecutedBy("mEncoderExecutor")
+        void setActive(boolean isActive) {
+            final State newState = isActive ? State.ACTIVE : State.INACTIVE;
+            if (mBufferProviderState == newState) {
+                return;
             }
-        }
+            mBufferProviderState = newState;
 
-        @GuardedBy("mLock")
-        private void match() {
-            if (!mListenerQueue.isEmpty() && !mFreeBufferIndexQueue.isEmpty()) {
-                Consumer<Integer> listener = mListenerQueue.poll();
-                Integer index = mFreeBufferIndexQueue.poll();
+            if (newState == State.INACTIVE) {
+                for (ListenableFuture<InputBuffer> future : mAcquisitionList) {
+                    future.cancel(true);
+                }
+                mAcquisitionList.clear();
+            }
+
+            for (Map.Entry<Observer<State>, Executor> entry : mStateObservers.entrySet()) {
                 try {
-                    mExecutor.execute(() -> listener.accept(index));
+                    entry.getValue().execute(() -> entry.getKey().onNewData(newState));
                 } catch (RejectedExecutionException e) {
                     Logger.e(TAG, "Unable to post to the supplied executor.", e);
-                    putFreeBufferIndex(index);
                 }
             }
         }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBuffer.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBuffer.java
new file mode 100644
index 0000000..fba76c7
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBuffer.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.encoder;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+
+/**
+ * InputBuffer is provided by the {@link Encoder} and used to feed data into the {@link Encoder}.
+ *
+ * <p>Once {@link InputBuffer} is complete or no longer needed, {@link #submit} or
+ * {@link #cancel} must be called to return the request to the encoder, otherwise, it will cause
+ * leakage or failure.
+ */
+public interface InputBuffer {
+
+    /**
+     * Gets the {@link ByteBuffer} of the input buffer.
+     *
+     * <p>Before submitting the InputBuffer, the internal position of the ByteBuffer must be set
+     * to prepare it for reading, e.g. the {@link ByteBuffer#position} is the beginning of the
+     * data, usually 0; the {@link ByteBuffer#limit} is the end of the data. Usually
+     * {@link ByteBuffer#flip} is used after writing data.
+     *
+     * <p>Getting ByteBuffer multiple times won't reset its internal position and data.
+     *
+     * @throws {@link IllegalStateException} if InputBuffer is submitted or canceled.
+     */
+    @NonNull
+    ByteBuffer getByteBuffer();
+
+    /**
+     * Sets the timestamp of the input buffer in microseconds.
+     *
+     * @throws {@link IllegalStateException} if InputBuffer is submitted or canceled.
+     */
+    void setPresentationTimeUs(long presentationTimeUs);
+
+    /**
+     * Denotes the input buffer is the end of the data stream.
+     *
+     * @throws {@link IllegalStateException} if InputBuffer is submitted or canceled.
+     */
+    void setEndOfStream(boolean isEndOfStream);
+
+    /**
+     * Submits the input buffer.
+     *
+     * <p>The data will be written to encoder only when {@link #submit} is called.
+     *
+     * @return {@code true} if submit successfully; {@code false} if already submitted, failed or
+     * has been canceled.
+     */
+    boolean submit();
+
+    /**
+     * Returns the request to encoder without taking any effect.
+     *
+     * @return {@code true} if cancel successfully; {@code false} if already submitted, failed or
+     * has been canceled.
+     */
+    boolean cancel();
+
+    /**
+     * The {@link ListenableFuture} that is complete when {@link #submit} or {@link #cancel} is
+     * called.
+     */
+    @NonNull
+    ListenableFuture<Void> getTerminationFuture();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBufferImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBufferImpl.java
new file mode 100644
index 0000000..3f88d73
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBufferImpl.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.encoder;
+
+import android.media.MediaCodec;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+class InputBufferImpl implements InputBuffer {
+    private final MediaCodec mMediaCodec;
+    private final int mBufferIndex;
+    private final ByteBuffer mByteBuffer;
+    private final ListenableFuture<Void> mTerminationFuture;
+    private final CallbackToFutureAdapter.Completer<Void> mTerminationCompleter;
+    private final AtomicBoolean mTerminated = new AtomicBoolean(false);
+    private long mPresentationTimeUs = 0L;
+    private boolean mIsEndOfStream = false;
+
+    InputBufferImpl(@NonNull MediaCodec mediaCodec, @IntRange(from = 0) int bufferIndex)
+            throws MediaCodec.CodecException {
+        mMediaCodec = Preconditions.checkNotNull(mediaCodec);
+        mBufferIndex = Preconditions.checkArgumentNonnegative(bufferIndex);
+        mByteBuffer = mediaCodec.getInputBuffer(bufferIndex);
+        AtomicReference<Completer<Void>> ref = new AtomicReference<>();
+        mTerminationFuture = CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    ref.set(completer);
+                    return "Terminate InputBuffer";
+                });
+        mTerminationCompleter = Preconditions.checkNotNull(ref.get());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @NonNull
+    public ByteBuffer getByteBuffer() {
+        throwIfTerminated();
+        return mByteBuffer;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setPresentationTimeUs(long presentationTimeUs) {
+        throwIfTerminated();
+        Preconditions.checkArgument(presentationTimeUs >= 0L);
+        mPresentationTimeUs = presentationTimeUs;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setEndOfStream(boolean endOfStream) {
+        throwIfTerminated();
+        mIsEndOfStream = endOfStream;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean submit() {
+        if (mTerminated.getAndSet(true)) {
+            return false;
+        }
+        try {
+            mMediaCodec.queueInputBuffer(mBufferIndex,
+                    mByteBuffer.position(),
+                    mByteBuffer.limit(),
+                    mPresentationTimeUs,
+                    mIsEndOfStream ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+            mTerminationCompleter.set(null);
+            return true;
+        } catch (IllegalStateException e) {
+            mTerminationCompleter.setException(e);
+            return false;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean cancel() {
+        if (mTerminated.getAndSet(true)) {
+            return false;
+        }
+        try {
+            mMediaCodec.queueInputBuffer(mBufferIndex, 0, 0, 0, 0);
+            mTerminationCompleter.set(null);
+        } catch (IllegalStateException e) {
+            mTerminationCompleter.setException(e);
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @NonNull
+    public ListenableFuture<Void> getTerminationFuture() {
+        return Futures.nonCancellationPropagating(mTerminationFuture);
+    }
+
+    private void throwIfTerminated() {
+        if (mTerminated.get()) {
+            throw new IllegalStateException("The buffer is submitted or canceled.");
+        }
+    }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
index 5c97071..085efd3 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.os.Build
 import android.os.Looper
+import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraX
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.impl.CameraFactory
@@ -54,7 +55,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
                     camera
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
index cf225ff..1af8754 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
@@ -36,9 +36,9 @@
  */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class PreviewTransformationDeviceTest {
+public class PreviewTransformationDeviceTest {
 
-    companion object {
+    public companion object {
         // Size of the PreviewView. Aspect ratio 2:1.
         private val PREVIEW_VIEW_SIZE = Size(400, 200)
         private val PIVOTED_PREVIEW_VIEW_SIZE =
@@ -71,13 +71,13 @@
     private lateinit var mView: View
 
     @Before
-    fun setUp() {
+    public fun setUp() {
         mPreviewTransform = PreviewTransformation()
         mView = View(ApplicationProvider.getApplicationContext())
     }
 
     @Test
-    fun cropRectWidthOffByOnePixel_match() {
+    public fun cropRectWidthOffByOnePixel_match() {
         assertThat(
             isCropRectAspectRatioMatchPreviewView(
                 Rect(
@@ -91,7 +91,7 @@
     }
 
     @Test
-    fun cropRectWidthOffByTwoPixels_mismatch() {
+    public fun cropRectWidthOffByTwoPixels_mismatch() {
         assertThat(
             isCropRectAspectRatioMatchPreviewView(
                 Rect(
@@ -115,7 +115,7 @@
     }
 
     @Test
-    fun correctTextureViewWith0Rotation() {
+    public fun correctTextureViewWith0Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_0)).isEqualTo(
             floatArrayOf(
                 0f,
@@ -131,7 +131,7 @@
     }
 
     @Test
-    fun correctTextureViewWith90Rotation() {
+    public fun correctTextureViewWith90Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_90)).isEqualTo(
             floatArrayOf(
                 0f,
@@ -147,7 +147,7 @@
     }
 
     @Test
-    fun correctTextureViewWith180Rotation() {
+    public fun correctTextureViewWith180Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_180)).isEqualTo(
             floatArrayOf(
                 SURFACE_SIZE.width.toFloat(),
@@ -163,7 +163,7 @@
     }
 
     @Test
-    fun correctTextureViewWith270Rotation() {
+    public fun correctTextureViewWith270Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_270)).isEqualTo(
             floatArrayOf(
                 SURFACE_SIZE.width.toFloat(),
@@ -196,7 +196,7 @@
     }
 
     @Test
-    fun ratioMatch_surfaceIsScaledToFillPreviewView() {
+    public fun ratioMatch_surfaceIsScaledToFillPreviewView() {
         // Arrange.
         mPreviewTransform.setTransformationInfo(
             SurfaceRequest.TransformationInfo.of(
@@ -225,7 +225,7 @@
     }
 
     @Test
-    fun mismatchedCropRect_fitStart() {
+    public fun mismatchedCropRect_fitStart() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FIT_START,
             LayoutDirection.LTR,
@@ -237,7 +237,7 @@
     }
 
     @Test
-    fun mismatchedCropRect_fitCenter() {
+    public fun mismatchedCropRect_fitCenter() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FIT_CENTER,
             LayoutDirection.LTR,
@@ -249,7 +249,7 @@
     }
 
     @Test
-    fun mismatchedCropRect_fitEnd() {
+    public fun mismatchedCropRect_fitEnd() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FIT_END,
             LayoutDirection.LTR,
@@ -261,7 +261,7 @@
     }
 
     @Test
-    fun mismatchedCropRectFrontCamera_fitStart() {
+    public fun mismatchedCropRectFrontCamera_fitStart() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FIT_START,
             LayoutDirection.LTR,
@@ -273,7 +273,7 @@
     }
 
     @Test
-    fun mismatchedCropRect_fillStart() {
+    public fun mismatchedCropRect_fillStart() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FILL_START,
             LayoutDirection.LTR,
@@ -285,7 +285,7 @@
     }
 
     @Test
-    fun mismatchedCropRect_fillCenter() {
+    public fun mismatchedCropRect_fillCenter() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FILL_CENTER,
             LayoutDirection.LTR,
@@ -297,7 +297,7 @@
     }
 
     @Test
-    fun mismatchedCropRect_fillEnd() {
+    public fun mismatchedCropRect_fillEnd() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FILL_END,
             LayoutDirection.LTR,
@@ -309,7 +309,7 @@
     }
 
     @Test
-    fun mismatchedCropRect_fitStartWithRtl_actsLikeFitEnd() {
+    public fun mismatchedCropRect_fitStartWithRtl_actsLikeFitEnd() {
         assertForMismatchedCropRect(
             PreviewView.ScaleType.FIT_START,
             LayoutDirection.RTL,
@@ -347,7 +347,7 @@
     }
 
     @Test
-    fun frontCamera0_transformationIsMirrored() {
+    public fun frontCamera0_transformationIsMirrored() {
         testOffCenterCropRectMirroring(FRONT_CAMERA, CROP_RECT_0, PREVIEW_VIEW_SIZE, 0)
 
         // Assert:
@@ -358,7 +358,7 @@
     }
 
     @Test
-    fun backCamera0_transformationIsNotMirrored() {
+    public fun backCamera0_transformationIsNotMirrored() {
         testOffCenterCropRectMirroring(BACK_CAMERA, CROP_RECT_0, PREVIEW_VIEW_SIZE, 0)
 
         // Assert:
@@ -369,7 +369,7 @@
     }
 
     @Test
-    fun frontCameraRotated90_transformationIsMirrored() {
+    public fun frontCameraRotated90_transformationIsMirrored() {
         testOffCenterCropRectMirroring(
             FRONT_CAMERA, CROP_RECT_90, PIVOTED_PREVIEW_VIEW_SIZE, 90
         )
@@ -382,7 +382,7 @@
     }
 
     @Test
-    fun backCameraRotated90_transformationIsNotMirrored() {
+    public fun backCameraRotated90_transformationIsNotMirrored() {
         testOffCenterCropRectMirroring(BACK_CAMERA, CROP_RECT_90, PIVOTED_PREVIEW_VIEW_SIZE, 90)
 
         // Assert:
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.java b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.java
index b8a1f6e..1d086ff 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.java
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.java
@@ -35,10 +35,10 @@
 import androidx.camera.view.PreviewView.ImplementationMode;
 import androidx.lifecycle.Observer;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
 
 import org.junit.After;
 import org.junit.Assume;
@@ -59,8 +59,9 @@
 public class PreviewViewBitmapTest {
 
     @Rule
-    public final ActivityTestRule<FakeActivity> mActivityRule =
-            new ActivityTestRule<>(FakeActivity.class);
+    public final ActivityScenarioRule<FakeActivity> mActivityRule = new ActivityScenarioRule<>(
+            FakeActivity.class);
+
     @Rule
     public final TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest();
 
@@ -87,6 +88,7 @@
         if (mCameraProvider != null) {
             runOnMainThread(() -> mCameraProvider.unbindAll());
             mCameraProvider.shutdown().get();
+            mCameraProvider = null;
         }
     }
 
@@ -109,7 +111,6 @@
         final FakeLifecycleOwner lifecycleOwner = new FakeLifecycleOwner();
         lifecycleOwner.startAndResume();
 
-
         runOnMainThread(() -> {
             // Act.
             preview.setSurfaceProvider(previewView.getSurfaceProvider());
@@ -277,7 +278,8 @@
             PreviewView previewView = new PreviewView(ApplicationProvider.getApplicationContext());
             previewView.setImplementationMode(mode);
             previewView.setScaleType(scaleType);
-            mActivityRule.getActivity().setContentView(previewView);
+            mActivityRule.getScenario().onActivity(
+                    activity -> activity.setContentView(previewView));
             previewViewAtomicReference.set(previewView);
         });
         return previewViewAtomicReference.get();
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
index 0e9e196..ef4277f 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
@@ -33,7 +33,7 @@
  */
 @SmallTest
 @RunWith(Parameterized::class)
-class PreviewViewMeteringPointFactoryDeviceTest(
+public class PreviewViewMeteringPointFactoryDeviceTest(
     private val cropRect: Rect,
     private val rotationDegrees: Int,
     private val surfaceSize: Size,
@@ -45,7 +45,7 @@
     private val expectedMeteringPoint: PointF
 ) {
 
-    companion object {
+    public companion object {
 
         private const val FRONT_CAMERA = true
         private const val BACK_CAMERA = false
@@ -73,7 +73,7 @@
 
         @JvmStatic
         @Parameterized.Parameters
-        fun data(): Collection<Array<Any>> {
+        public fun data(): Collection<Array<Any>> {
 
             return listOf(
                 // Device in sensor orientation without crop rect.
@@ -195,7 +195,7 @@
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
 
     @Test
-    fun verifyMeteringPoint() {
+    public fun verifyMeteringPoint() {
         // Arrange.
         val previewTransformation = PreviewTransformation()
         previewTransformation.scaleType = scaleType
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt
index cccc54e..62990f0 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt
@@ -16,9 +16,7 @@
 
 package androidx.camera.view
 
-import android.app.Activity
 import android.content.Context
-import android.view.View
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraX
@@ -32,6 +30,7 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.Observer
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
@@ -49,18 +48,19 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-class PreviewViewStreamStateTest(private val implMode: PreviewView.ImplementationMode) {
-    companion object {
+public class PreviewViewStreamStateTest(private val implMode: PreviewView.ImplementationMode) {
+
+    public companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
-        fun data() = arrayOf(
+        public fun data(): Array<PreviewView.ImplementationMode> = arrayOf(
             PreviewView.ImplementationMode.COMPATIBLE,
             PreviewView.ImplementationMode.PERFORMANCE
         )
 
         @BeforeClass
         @JvmStatic
-        fun classSetUp() {
+        public fun classSetUp() {
             CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
         }
     }
@@ -72,49 +72,37 @@
     private lateinit var mCameraProvider: ProcessCameraProvider
 
     @get:Rule
-    val mUseCamera: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
+    public val mUseCamera: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
 
-    @Suppress("DEPRECATION")
     @get:Rule
-    var mActivityRule = androidx.test.rule.ActivityTestRule(
-        FakeActivity::class.java
-    )
-
-    @Throws(Throwable::class)
-    private fun setContentView(view: View) {
-        val activity: Activity = mActivityRule.activity
-        mActivityRule.runOnUiThread { activity.setContentView(view) }
-    }
+    public val mActivityRule: ActivityScenarioRule<FakeActivity> =
+        ActivityScenarioRule(FakeActivity::class.java)
 
     @Before
-    fun setUp() {
+    public fun setUp() {
         Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
         CoreAppTestUtil.assumeCompatibleDevice()
 
-        mIsSetup = true
-
         val context = ApplicationProvider.getApplicationContext<Context>()
         val config = Camera2Config.defaultConfig()
         CameraX.initialize(context, config)
-        mLifecycle = FakeLifecycleOwner()
-        mInstrumentation.runOnMainSync {
-            mPreviewView = PreviewView(context)
-        }
-        setContentView(mPreviewView)
-        mInstrumentation.runOnMainSync {
-            mPreviewView.implementationMode = implMode
-        }
 
+        mLifecycle = FakeLifecycleOwner()
+        mActivityRule.scenario.onActivity { activity ->
+            mPreviewView = PreviewView(context)
+            mPreviewView.implementationMode = implMode
+            activity.setContentView(mPreviewView)
+        }
         mCameraProvider = ProcessCameraProvider.getInstance(context).get()
+        mIsSetup = true
     }
 
     @After
-    fun tearDown() {
+    public fun tearDown() {
         if (mIsSetup) {
-            mInstrumentation.runOnMainSync {
-                mCameraProvider.unbindAll()
-            }
+            mInstrumentation.runOnMainSync { mCameraProvider.unbindAll() }
             mCameraProvider.shutdown().get()
+            mIsSetup = false
         }
     }
 
@@ -134,7 +122,7 @@
     }
 
     @Test
-    fun streamState_IDLE_TO_STREAMING_startPreview() {
+    public fun streamState_IDLE_TO_STREAMING_startPreview() {
         assertStreamState(PreviewView.StreamState.IDLE)
 
         startPreview(mLifecycle, mPreviewView, CameraSelector.DEFAULT_BACK_CAMERA)
@@ -144,7 +132,7 @@
     }
 
     @Test
-    fun streamState_STREAMING_TO_IDLE_TO_STREAMING_lifecycleStopAndStart() {
+    public fun streamState_STREAMING_TO_IDLE_TO_STREAMING_lifecycleStopAndStart() {
         startPreview(mLifecycle, mPreviewView, CameraSelector.DEFAULT_BACK_CAMERA)
         mLifecycle.startAndResume()
         assertStreamState(PreviewView.StreamState.STREAMING)
@@ -157,7 +145,7 @@
     }
 
     @Test
-    fun streamState_STREAMING_TO_IDLE_unbindAll() {
+    public fun streamState_STREAMING_TO_IDLE_unbindAll() {
         startPreview(mLifecycle, mPreviewView, CameraSelector.DEFAULT_BACK_CAMERA)
         mLifecycle.startAndResume()
         assertStreamState(PreviewView.StreamState.STREAMING)
@@ -167,7 +155,7 @@
     }
 
     @Test
-    fun streamState_STREAMING_TO_IDLE_unbindPreviewOnly() {
+    public fun streamState_STREAMING_TO_IDLE_unbindPreviewOnly() {
         val preview = startPreview(mLifecycle, mPreviewView, CameraSelector.DEFAULT_BACK_CAMERA)
 
         mLifecycle.startAndResume()
@@ -178,7 +166,7 @@
     }
 
     @Test
-    fun streamState_STREAMING_TO_IDLE_TO_STREAMING_switchCamera() {
+    public fun streamState_STREAMING_TO_IDLE_TO_STREAMING_switchCamera() {
         Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
 
         startPreview(mLifecycle, mPreviewView, CameraSelector.DEFAULT_BACK_CAMERA)
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
index 8e8b65a..4c81f04 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
@@ -66,11 +66,11 @@
 import androidx.camera.view.test.R;
 import androidx.core.content.ContextCompat;
 import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ActivityScenario;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObjectNotFoundException;
 import androidx.test.uiautomator.UiSelector;
@@ -107,37 +107,16 @@
     public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest();
 
     private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
-    @Rule
-    public final ActivityTestRule<FakeActivity> mActivityRule = new ActivityTestRule<>(
-            FakeActivity.class, false, false);
+    private ActivityScenario<FakeActivity> mActivityScenario;
     private final Context mContext = ApplicationProvider.getApplicationContext();
-    private List<SurfaceRequest> mSurfaceRequestList = new ArrayList<>();
+    private final List<SurfaceRequest> mSurfaceRequestList = new ArrayList<>();
     private PreviewView mPreviewView;
     private MeteringPointFactory mMeteringPointFactory;
 
-    private SurfaceRequest createSurfaceRequest(CameraInfo cameraInfo,
-            boolean isRGBA8888Required) {
-        return createSurfaceRequest(DEFAULT_SURFACE_SIZE, cameraInfo, isRGBA8888Required);
-    }
-
-    private SurfaceRequest createSurfaceRequest(CameraInfo cameraInfo) {
-        return createSurfaceRequest(DEFAULT_SURFACE_SIZE, cameraInfo, false);
-    }
-
-    private SurfaceRequest createSurfaceRequest(Size size, CameraInfo cameraInfo,
-            boolean isRGBA8888Required) {
-        FakeCamera fakeCamera = spy(new FakeCamera());
-        when(fakeCamera.getCameraInfo()).thenReturn(cameraInfo);
-
-        SurfaceRequest surfaceRequest = new SurfaceRequest(size, fakeCamera, isRGBA8888Required);
-        mSurfaceRequestList.add(surfaceRequest);
-        return surfaceRequest;
-    }
-
     @Before
     public void setUp() throws CoreAppTestUtil.ForegroundOccupiedError {
         CoreAppTestUtil.prepareDeviceUI(mInstrumentation);
-        mActivityRule.launchActivity(null);
+        mActivityScenario = ActivityScenario.launch(FakeActivity.class);
     }
 
     @After
@@ -149,20 +128,6 @@
         }
     }
 
-    private CameraInfo createCameraInfo(String implementationType) {
-        FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal();
-        cameraInfoInternal.setImplementationType(implementationType);
-        return cameraInfoInternal;
-    }
-
-    private CameraInfo createCameraInfo(int rotationDegrees, String implementationType,
-            @CameraSelector.LensFacing int lensFacing) {
-        FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal(rotationDegrees,
-                lensFacing);
-        cameraInfoInternal.setImplementationType(implementationType);
-        return cameraInfoInternal;
-    }
-
     @Test
     public void previewViewPinched_pinchToZoomInvokedOnController()
             throws InterruptedException, UiObjectNotFoundException {
@@ -173,7 +138,7 @@
         // Arrange.
         CountDownLatch countDownLatch = new CountDownLatch(1);
         Semaphore semaphore = new Semaphore(0);
-        CameraController fakeController = new CameraController(mInstrumentation.getContext()) {
+        CameraController fakeController = new CameraController(mContext) {
             @Override
             void onPinchToZoom(float pinchToZoomScale) {
                 semaphore.release();
@@ -209,7 +174,7 @@
         // Arrange.
         CountDownLatch countDownLatch = new CountDownLatch(1);
         Semaphore semaphore = new Semaphore(0);
-        CameraController fakeController = new CameraController(mInstrumentation.getContext()) {
+        CameraController fakeController = new CameraController(mContext) {
             @Override
             void onTapToFocus(MeteringPointFactory meteringPointFactory, float x, float y) {
                 semaphore.release();
@@ -368,20 +333,16 @@
     @Test
     public void correctSurfacePixelFormat_whenRGBA8888IsRequired() throws Throwable {
         final CameraInfo cameraInfo = createCameraInfo(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
-        SurfaceRequest surfaceRequest = createSurfaceRequest(cameraInfo, true);
+        SurfaceRequest surfaceRequest = createRgb8888SurfaceRequest(cameraInfo);
         ListenableFuture<Surface> future = surfaceRequest.getDeferrableSurface().getSurface();
 
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
+        mActivityScenario.onActivity(activity -> {
+            final PreviewView previewView = new PreviewView(mContext);
+            setContentView(previewView);
 
-                final PreviewView previewView = new PreviewView(mContext);
-                setContentView(previewView);
-
-                previewView.setImplementationMode(PERFORMANCE);
-                Preview.SurfaceProvider surfaceProvider = previewView.getSurfaceProvider();
-                surfaceProvider.onSurfaceRequested(surfaceRequest);
-            }
+            previewView.setImplementationMode(PERFORMANCE);
+            Preview.SurfaceProvider surfaceProvider = previewView.getSurfaceProvider();
+            surfaceProvider.onSurfaceRequested(surfaceRequest);
         });
         final Surface[] surface = new Surface[1];
         CountDownLatch countDownLatch = new CountDownLatch(1);
@@ -657,12 +618,12 @@
     }
 
     @Test
-    public void redrawsPreview_whenLayoutResized() throws Throwable {
+    public void redrawsPreview_whenLayoutResized() {
         final AtomicReference<PreviewView> previewView = new AtomicReference<>();
         final AtomicReference<FrameLayout> container = new AtomicReference<>();
         final PreviewViewImplementation implementation = mock(TestPreviewViewImplementation.class);
 
-        mActivityRule.runOnUiThread(() -> {
+        mActivityScenario.onActivity(activity -> {
             previewView.set(new PreviewView(mContext));
             previewView.get().mImplementation = implementation;
 
@@ -681,12 +642,12 @@
     }
 
     @Test
-    public void doesNotRedrawPreview_whenDetachedFromWindow() throws Throwable {
+    public void doesNotRedrawPreview_whenDetachedFromWindow() {
         final AtomicReference<PreviewView> previewView = new AtomicReference<>();
         final AtomicReference<FrameLayout> container = new AtomicReference<>();
         final PreviewViewImplementation implementation = mock(TestPreviewViewImplementation.class);
 
-        mActivityRule.runOnUiThread(() -> {
+        mActivityScenario.onActivity(activity -> {
             previewView.set(new PreviewView(mContext));
             previewView.get().mImplementation = implementation;
 
@@ -707,12 +668,12 @@
     }
 
     @Test
-    public void redrawsPreview_whenReattachedToWindow() throws Throwable {
+    public void redrawsPreview_whenReattachedToWindow() {
         final AtomicReference<PreviewView> previewView = new AtomicReference<>();
         final AtomicReference<FrameLayout> container = new AtomicReference<>();
         final PreviewViewImplementation implementation = mock(TestPreviewViewImplementation.class);
 
-        mActivityRule.runOnUiThread(() -> {
+        mActivityScenario.onActivity(activity -> {
             previewView.set(new PreviewView(mContext));
             previewView.get().mImplementation = implementation;
 
@@ -793,7 +754,39 @@
     }
 
     private void setContentView(View view) {
-        mActivityRule.getActivity().setContentView(view);
+        mActivityScenario.onActivity(activity -> activity.setContentView(view));
+    }
+
+    private SurfaceRequest createRgb8888SurfaceRequest(CameraInfo cameraInfo) {
+        return createSurfaceRequest(cameraInfo, true);
+    }
+
+    private SurfaceRequest createSurfaceRequest(CameraInfo cameraInfo) {
+        return createSurfaceRequest(cameraInfo, false);
+    }
+
+    private SurfaceRequest createSurfaceRequest(CameraInfo cameraInfo, boolean isRGBA8888Required) {
+        final FakeCamera fakeCamera = spy(new FakeCamera());
+        when(fakeCamera.getCameraInfo()).thenReturn(cameraInfo);
+
+        final SurfaceRequest surfaceRequest = new SurfaceRequest(DEFAULT_SURFACE_SIZE, fakeCamera,
+                isRGBA8888Required);
+        mSurfaceRequestList.add(surfaceRequest);
+        return surfaceRequest;
+    }
+
+    private CameraInfo createCameraInfo(String implementationType) {
+        FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal();
+        cameraInfoInternal.setImplementationType(implementationType);
+        return cameraInfoInternal;
+    }
+
+    private CameraInfo createCameraInfo(int rotationDegrees, String implementationType,
+            @CameraSelector.LensFacing int lensFacing) {
+        FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal(rotationDegrees,
+                lensFacing);
+        cameraInfoInternal.setImplementationType(implementationType);
+        return cameraInfoInternal;
     }
 
     private void updateCropRectAndWaitForIdle(Rect cropRect) {
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
index 4356d85..cc960aa 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.camera.view
 
-import android.app.Activity
 import android.content.Context
 import android.util.Size
 import android.view.View
@@ -25,6 +24,7 @@
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.fakes.FakeActivity
 import androidx.camera.testing.fakes.FakeCamera
+import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -32,7 +32,6 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
@@ -40,37 +39,29 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-class SurfaceViewImplementationTest {
+public class SurfaceViewImplementationTest {
 
-    private val ANY_WIDTH = 640
-    private val ANY_HEIGHT = 480
-    private val ANY_SIZE = Size(ANY_WIDTH, ANY_HEIGHT)
+    public companion object {
+        private const val ANY_WIDTH = 640
+        private const val ANY_HEIGHT = 480
+        private val ANY_SIZE = Size(ANY_WIDTH, ANY_HEIGHT)
+    }
 
     private lateinit var mParent: FrameLayout
     private lateinit var mImplementation: SurfaceViewImplementation
-    private val mInstrumentation =
-        InstrumentationRegistry.getInstrumentation()
+    private val mInstrumentation = InstrumentationRegistry.getInstrumentation()
     private lateinit var mSurfaceRequest: SurfaceRequest
     private lateinit var mContext: Context
 
     // Shows the view in activity so that SurfaceView can work normally
-    @Suppress("DEPRECATION")
-    @get:Rule
-    var mActivityRule = androidx.test.rule.ActivityTestRule(
-        FakeActivity::class.java, false, false
-    )
-
-    @Throws(Throwable::class)
-    private fun setContentView(view: View) {
-        val activity: Activity = mActivityRule.activity
-        mActivityRule.runOnUiThread { activity.setContentView(view) }
-    }
+    private lateinit var mActivityScenario: ActivityScenario<FakeActivity>
 
     @Before
-    fun setUp() {
+    public fun setUp() {
         CoreAppTestUtil.prepareDeviceUI(mInstrumentation)
-        mActivityRule.launchActivity(null)
-        mContext = ApplicationProvider.getApplicationContext<Context>()
+
+        mActivityScenario = ActivityScenario.launch(FakeActivity::class.java)
+        mContext = ApplicationProvider.getApplicationContext()
         mParent = FrameLayout(mContext)
         setContentView(mParent)
 
@@ -79,12 +70,12 @@
     }
 
     @After
-    fun tearDown() {
+    public fun tearDown() {
         mSurfaceRequest.deferrableSurface.close()
     }
 
     @Test
-    fun surfaceProvidedSuccessfully() {
+    public fun surfaceProvidedSuccessfully() {
         CoreAppTestUtil.checkKeyguard(mContext)
 
         mInstrumentation.runOnMainSync {
@@ -96,10 +87,10 @@
     }
 
     @Test
-    fun onSurfaceNotInUseListener_isCalledWhenSurfaceIsNotUsedAnyMore() {
+    public fun onSurfaceNotInUseListener_isCalledWhenSurfaceIsNotUsedAnyMore() {
         CoreAppTestUtil.checkKeyguard(mContext)
 
-        var listenerLatch = CountDownLatch(1)
+        val listenerLatch = CountDownLatch(1)
         val onSurfaceNotInUseListener = {
             listenerLatch.countDown()
         }
@@ -114,14 +105,14 @@
     }
 
     @Test
-    fun onSurfaceNotInUseListener_isCalledWhenSurfaceRequestIsCancelled() {
-        var listenerLatch = CountDownLatch(1)
+    public fun onSurfaceNotInUseListener_isCalledWhenSurfaceRequestIsCancelled() {
+        val listenerLatch = CountDownLatch(1)
         val onSurfaceNotInUseListener = {
             listenerLatch.countDown()
         }
 
         // Not attach the mParent to the window so that the Surface cannot be created.
-        setContentView(View(ApplicationProvider.getApplicationContext()))
+        setContentView(View(mContext))
 
         mInstrumentation.runOnMainSync {
             mImplementation.onSurfaceRequested(mSurfaceRequest, onSurfaceNotInUseListener)
@@ -135,8 +126,13 @@
     }
 
     @Test
-    fun waitForNextFrame_futureCompletesImmediately() {
+    public fun waitForNextFrame_futureCompletesImmediately() {
         val future = mImplementation.waitForNextFrame()
         future.get(20, TimeUnit.MILLISECONDS)
     }
+
+    @Throws(Throwable::class)
+    private fun setContentView(view: View) {
+        mActivityScenario.onActivity { activity -> activity.setContentView(view) }
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.java b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.java
index 1074d8d..961cab6 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.java
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.java
@@ -149,7 +149,7 @@
         SurfaceRequest surfaceRequest = getSurfaceRequest();
         CountDownLatch latchForSurfaceNotInUse = new CountDownLatch(1);
         PreviewViewImplementation.OnSurfaceNotInUseListener onSurfaceNotInUseListener =
-                () -> latchForSurfaceNotInUse.countDown();
+                latchForSurfaceNotInUse::countDown;
 
         mImplementation.onSurfaceRequested(surfaceRequest, onSurfaceNotInUseListener);
         DeferrableSurface deferrableSurface = surfaceRequest.getDeferrableSurface();
@@ -172,7 +172,7 @@
         SurfaceRequest surfaceRequest = getSurfaceRequest();
         CountDownLatch latchForSurfaceNotInUse = new CountDownLatch(1);
         PreviewViewImplementation.OnSurfaceNotInUseListener onSurfaceNotInUseListener =
-                () -> latchForSurfaceNotInUse.countDown();
+                latchForSurfaceNotInUse::countDown;
 
         mImplementation.onSurfaceRequested(surfaceRequest, onSurfaceNotInUseListener);
         DeferrableSurface deferrableSurface = surfaceRequest.getDeferrableSurface();
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 21e6f30..46089e9 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -114,6 +114,7 @@
      */
     @UseExperimental(markerClass = ExperimentalVideo.class)
     @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
     @IntDef(flag = true, value = {IMAGE_CAPTURE, IMAGE_ANALYSIS, VIDEO_CAPTURE})
     public @interface UseCases {
     }
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index a304989..bee7e6ae 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -69,13 +69,13 @@
     implementation(project(":camera:camera-camera2-pipe-integration"))
     implementation(project(":camera:camera-core"))
     implementation(project(":camera:camera-lifecycle"))
+    implementation(project(":appcompat:appcompat"))
+    implementation(project(":activity:activity"))
+    implementation(project(":fragment:fragment"))
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
 
     // Android Support Library
     api(CONSTRAINT_LAYOUT, { transitive = true })
-    implementation("androidx.appcompat:appcompat:1.1.0")
-    implementation("androidx.activity:activity:1.2.0-alpha05")
-    implementation("androidx.fragment:fragment:1.3.0-alpha05")
     implementation(GUAVA_ANDROID)
     implementation(ESPRESSO_IDLING_RESOURCE)
 
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index 173165f..e833abd 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -115,11 +115,6 @@
         image.close();
     };
 
-    @Override
-    public void onAttach(@NonNull Context context) {
-        super.onAttach(context);
-    }
-
     @NonNull
     @Override
     @UseExperimental(markerClass = ExperimentalVideo.class)
@@ -241,7 +236,7 @@
         view.findViewById(R.id.video_record).setOnClickListener(v -> {
             try {
                 String videoFileName = "video_" + System.currentTimeMillis();
-                ContentResolver resolver = getContext().getContentResolver();
+                ContentResolver resolver = requireContext().getContentResolver();
                 ContentValues contentValues = new ContentValues();
                 contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
                 contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
@@ -326,7 +321,9 @@
         if (mExecutorService != null) {
             mExecutorService.shutdown();
         }
-        mSensorRotationListener.disable();
+        if (mSensorRotationListener != null) {
+            mSensorRotationListener.disable();
+        }
     }
 
     void checkFailedFuture(ListenableFuture<Void> voidFuture) {
@@ -347,7 +344,7 @@
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
     void toast(String message) {
-        getActivity().runOnUiThread(
+        requireActivity().runOnUiThread(
                 () -> Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show());
     }
 
@@ -373,8 +370,9 @@
     @UseExperimental(markerClass = ExperimentalVideo.class)
     private void updateUiText() {
         mFlashMode.setText(getFlashModeTextResId());
-        mCameraToggle.setChecked(mCameraController.getCameraSelector().getLensFacing()
-                == CameraSelector.LENS_FACING_BACK);
+        final Integer lensFacing = mCameraController.getCameraSelector().getLensFacing();
+        mCameraToggle.setChecked(
+                lensFacing != null && lensFacing == CameraSelector.LENS_FACING_BACK);
         mVideoEnabledToggle.setChecked(mCameraController.isVideoCaptureEnabled());
         mPinchToZoomToggle.setChecked(mCameraController.isPinchToZoomEnabled());
         mTapToFocusToggle.setChecked(mCameraController.isTapToFocusEnabled());
@@ -510,7 +508,7 @@
         contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
         ImageCapture.OutputFileOptions outputFileOptions =
                 new ImageCapture.OutputFileOptions.Builder(
-                        getContext().getContentResolver(),
+                        requireContext().getContentResolver(),
                         MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                         contentValues).build();
         mCameraController.takePicture(outputFileOptions, mExecutorService, callback);
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
index 74878d8..c23de8b 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
@@ -17,6 +17,7 @@
 package androidx.camera.integration.view;
 
 import android.Manifest;
+import android.annotation.SuppressLint;
 import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.Bundle;
@@ -28,7 +29,9 @@
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 import androidx.fragment.app.Fragment;
 
@@ -53,7 +56,7 @@
     private Mode mMode = Mode.CAMERA_VIEW;
 
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         // Get extra option for checking whether it need to be implemented with PreviewView
@@ -77,7 +80,8 @@
                 if (allPermissionsGranted()) {
                     startFragment();
                 } else if (!mCheckedPermissions) {
-                    requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
+                    ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS,
+                            REQUEST_CODE_PERMISSIONS);
                     mCheckedPermissions = true;
                 }
             } else {
@@ -105,6 +109,7 @@
         return true;
     }
 
+    @SuppressLint("NonConstantResourceId")
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
         switch (item.getItemId()) {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
index 97e5053..9b1a8e2 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
@@ -68,7 +68,8 @@
 public class PreviewViewFragment extends Fragment {
 
     /** Scale types of ImageView that map to the PreviewView scale types. */
-    private static final ImageView.ScaleType[] IMAGE_VIEW_SCALE_TYPES =
+    // Synthetic access
+    static final ImageView.ScaleType[] IMAGE_VIEW_SCALE_TYPES =
             {ImageView.ScaleType.FIT_CENTER, ImageView.ScaleType.FIT_CENTER,
                     ImageView.ScaleType.FIT_CENTER, ImageView.ScaleType.FIT_START,
                     ImageView.ScaleType.FIT_CENTER, ImageView.ScaleType.FIT_END};
@@ -182,7 +183,8 @@
         });
     }
 
-    void updateTargetRotationButtonText(Button rotationButton) {
+    @SuppressLint("SetTextI18n")
+    void updateTargetRotationButtonText(final @NonNull Button rotationButton) {
         switch (mPreview.getTargetRotation()) {
             case Surface.ROTATION_0:
                 rotationButton.setText("ROTATION_0");
@@ -225,7 +227,7 @@
             // Get extra option for setting initial camera direction
             boolean isCameraDirectionValid = false;
             String cameraDirectionString = null;
-            Bundle bundle = getActivity().getIntent().getExtras();
+            Bundle bundle = requireActivity().getIntent().getExtras();
             if (bundle != null) {
                 cameraDirectionString = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION);
                 isCameraDirectionValid =
@@ -330,6 +332,7 @@
         setUpFocusAndMetering(camera);
     }
 
+    @SuppressLint("ClickableViewAccessibility")
     private void setUpFocusAndMetering(@NonNull final Camera camera) {
         mPreviewView.setOnTouchListener((view, motionEvent) -> {
             switch (motionEvent.getAction()) {
@@ -414,7 +417,7 @@
     // like TextureView.SurfaceTextureListener#onSurfaceTextureUpdated but it will require to add
     // API in PreviewView which is not a good idea. And we use OnPreDrawListener instead of
     // OnDrawListener because OnDrawListener is not invoked on some low API level devices.
-    private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = () -> {
+    private final ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = () -> {
         if (mPreviewUpdatingLatch != null) {
             mPreviewUpdatingLatch.countDown();
         }
@@ -439,5 +442,5 @@
     void setPreviewUpdatingLatch(@NonNull CountDownLatch previewUpdatingLatch) {
         mPreviewUpdatingLatch = previewUpdatingLatch;
     }
-    // end region
+    // endregion
 }
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 5b604f6..827dffa 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -1,6 +1,12 @@
 // Signature format: 4.0
 package androidx.car.app {
 
+  public final class AppInfo {
+    method public int getLatestCarAppApiLevel();
+    method public String getLibraryVersion();
+    method public int getMinCarAppApiLevel();
+  }
+
   public class AppManager {
     method public void invalidate();
     method public void setSurfaceListener(androidx.car.app.SurfaceListener?);
@@ -14,37 +20,22 @@
     field public static final String NAVIGATION_TEMPLATES = "androidx.car.app.NAVIGATION_TEMPLATES";
   }
 
-  public abstract class CarAppService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+  public abstract class CarAppService extends android.app.Service {
     ctor public CarAppService();
-    method @CallSuper public void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
-    method public void finish();
-    method public final androidx.car.app.CarContext getCarContext();
-    method public androidx.car.app.HostInfo? getHostInfo();
-    method public androidx.lifecycle.Lifecycle getLifecycle();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
-    method public void onCarAppFinished();
+    method @CallSuper public final void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
+    method public final androidx.car.app.Session? getCurrentSession();
+    method public final androidx.car.app.HostInfo? getHostInfo();
+    method @CallSuper public final android.os.IBinder? onBind(android.content.Intent);
     method public void onCarConfigurationChanged(android.content.res.Configuration);
-    method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
-    method public final void onDestroy();
+    method public abstract androidx.car.app.Session onCreateSession();
     method public void onNewIntent(android.content.Intent);
     method public final boolean onUnbind(android.content.Intent);
-  }
-
-  public class CarAppVersion {
-    method public boolean isGreaterOrEqualTo(androidx.car.app.CarAppVersion);
-    method public static androidx.car.app.CarAppVersion? of(String) throws androidx.car.app.MalformedVersionException;
-    field public static final androidx.car.app.CarAppVersion HANDSHAKE_MIN_VERSION;
-    field public static final androidx.car.app.CarAppVersion INSTANCE;
-  }
-
-  public enum CarAppVersion.ReleaseSuffix {
-    method public static androidx.car.app.CarAppVersion.ReleaseSuffix fromString(String);
-    enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_BETA;
-    enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_EAP;
+    field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
   }
 
   public class CarContext extends android.content.ContextWrapper {
     method public void finishCarApp();
+    method public int getCarAppApiLevel();
     method public Object getCarService(String);
     method public <T> T getCarService(Class<T!>);
     method public String getCarServiceName(Class<?>);
@@ -53,11 +44,11 @@
     method public void startCarApp(android.content.Intent);
     method public static void startCarApp(android.content.Intent, android.content.Intent);
     field public static final String ACTION_NAVIGATE = "androidx.car.app.action.NAVIGATE";
-    field public static final String APP_SERVICE = "app_manager";
+    field public static final String APP_SERVICE = "app";
     field public static final String CAR_SERVICE = "car";
-    field public static final String NAVIGATION_SERVICE = "navigation_manager";
-    field public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
-    field public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+    field public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra.START_CAR_APP_BINDER_KEY";
+    field public static final String NAVIGATION_SERVICE = "navigation";
+    field public static final String SCREEN_SERVICE = "screen";
   }
 
   public final class CarToast {
@@ -89,18 +80,16 @@
   }
 
   public class HostInfo {
-    ctor public HostInfo(String, int);
     method public String getPackageName();
     method public int getUid();
   }
 
-  public class MalformedVersionException extends java.lang.Exception {
-    ctor public MalformedVersionException(String?);
-    ctor public MalformedVersionException(String, Throwable);
-    ctor public MalformedVersionException(Throwable?);
+  public interface OnDoneCallback {
+    method public void onFailure(androidx.car.app.serialization.Bundleable);
+    method public void onSuccess(androidx.car.app.serialization.Bundleable?);
   }
 
-  public interface OnScreenResultCallback {
+  public interface OnScreenResultListener {
     method public void onScreenResult(Object?);
   }
 
@@ -111,25 +100,27 @@
     method public final androidx.lifecycle.Lifecycle getLifecycle();
     method public String? getMarker();
     method public final androidx.car.app.ScreenManager getScreenManager();
-    method public abstract androidx.car.app.model.Template getTemplate();
     method public final void invalidate();
+    method public abstract androidx.car.app.model.Template onGetTemplate();
     method public void setMarker(String?);
     method public void setResult(Object?);
-    field public static final String ROOT = "ROOT";
   }
 
   public class ScreenManager {
     method public androidx.car.app.Screen getTop();
     method public void pop();
     method public void popTo(String);
+    method public void popToRoot();
     method public void push(androidx.car.app.Screen);
-    method public void pushForResult(androidx.car.app.Screen, androidx.car.app.OnScreenResultCallback);
+    method public void pushForResult(androidx.car.app.Screen, androidx.car.app.OnScreenResultListener);
     method public void remove(androidx.car.app.Screen);
   }
 
-  public interface SearchListener {
-    method public void onSearchSubmitted(String);
-    method public void onSearchTextChanged(String);
+  public abstract class Session implements androidx.lifecycle.LifecycleOwner {
+    ctor public Session();
+    method public final androidx.car.app.CarContext getCarContext();
+    method public androidx.lifecycle.Lifecycle getLifecycle();
+    method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
   }
 
   public class SurfaceContainer {
@@ -153,37 +144,10 @@
 
 }
 
-package androidx.car.app.host {
+package androidx.car.app.annotations {
 
-  public interface OnCheckedChangeListenerWrapper {
-    method public void onCheckedChange(boolean, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface OnDoneCallback {
-    method public void onFailure(androidx.car.app.serialization.Bundleable);
-    method public void onSuccess(androidx.car.app.serialization.Bundleable?);
-  }
-
-  public interface OnItemVisibilityChangedListenerWrapper {
-    method public void onItemVisibilityChanged(int, int, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface OnSelectedListenerWrapper {
-    method public void onSelected(int, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface SearchListenerWrapper {
-    method public void onSearchSubmitted(String, androidx.car.app.host.OnDoneCallback);
-    method public void onSearchTextChanged(String, androidx.car.app.host.OnDoneCallback);
-  }
-
-}
-
-package androidx.car.app.host.model {
-
-  public interface OnClickListenerWrapper {
-    method public boolean isParkedOnly();
-    method public void onClick(androidx.car.app.host.OnDoneCallback);
+  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public @interface RequiresCarApi {
+    method public abstract int value();
   }
 
 }
@@ -194,7 +158,7 @@
     method public static androidx.car.app.model.Action.Builder builder();
     method public androidx.car.app.model.CarColor getBackgroundColor();
     method public androidx.car.app.model.CarIcon? getIcon();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public androidx.car.app.model.CarText? getTitle();
     method public int getType();
     method public boolean isStandard();
@@ -231,7 +195,6 @@
     ctor public ActionStrip.Builder();
     method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
     method public androidx.car.app.model.ActionStrip build();
-    method public androidx.car.app.model.ActionStrip.Builder clearActions();
   }
 
   public class CarColor {
@@ -273,8 +236,6 @@
     field public static final int TYPE_CUSTOM = 1; // 0x1
     field public static final int TYPE_ERROR = 6; // 0x6
     field public static final int TYPE_UNKNOWN = 0; // 0x0
-    field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
-    field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
   }
 
   public static final class CarIcon.Builder {
@@ -358,9 +319,9 @@
     method public static androidx.car.app.model.GridItem.Builder builder();
     method public androidx.car.app.model.CarIcon getImage();
     method public int getImageType();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public androidx.car.app.model.CarText? getText();
-    method public androidx.car.app.model.CarText? getTitle();
+    method public androidx.car.app.model.CarText getTitle();
     method public androidx.car.app.model.Toggle? getToggle();
     field public static final int IMAGE_TYPE_ICON = 1; // 0x1
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -372,7 +333,7 @@
     method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
     method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
     method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
-    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
     method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
   }
 
@@ -404,10 +365,9 @@
     method public static androidx.car.app.model.ItemList.Builder builder();
     method public java.util.List<java.lang.Object!> getItems();
     method public androidx.car.app.model.CarText? getNoItemsMessage();
-    method public androidx.car.app.host.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangeListener();
-    method public androidx.car.app.host.OnSelectedListenerWrapper? getOnSelectedListener();
+    method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+    method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
     method public int getSelectedIndex();
-    method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
   }
 
   public static final class ItemList.Builder {
@@ -416,8 +376,8 @@
     method public androidx.car.app.model.ItemList build();
     method public androidx.car.app.model.ItemList.Builder clearItems();
     method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
-    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
-    method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangedListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+    method public androidx.car.app.model.ItemList.Builder setOnSelectedListener(androidx.car.app.model.ItemList.OnSelectedListener?);
     method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
   }
 
@@ -449,7 +409,6 @@
   public static final class ListTemplate.Builder {
     method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
     method public androidx.car.app.model.ListTemplate build();
-    method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
     method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
     method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
@@ -459,7 +418,7 @@
 
   public final class MessageTemplate implements androidx.car.app.model.Template {
     method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
-    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.ActionList? getActions();
     method public androidx.car.app.model.CarText? getDebugMessage();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public androidx.car.app.model.CarIcon? getIcon();
@@ -491,23 +450,38 @@
     method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
   }
 
+  public interface OnCheckedChangeListenerWrapper {
+    method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
+  }
+
   public interface OnClickListener {
     method public void onClick();
   }
 
+  public interface OnClickListenerWrapper {
+    method public boolean isParkedOnly();
+    method public void onClick(androidx.car.app.OnDoneCallback);
+  }
+
+  public interface OnItemVisibilityChangedListenerWrapper {
+    method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
+  }
+
+  public interface OnSelectedListenerWrapper {
+    method public void onSelected(int, androidx.car.app.OnDoneCallback);
+  }
+
   public final class Pane {
     method public static androidx.car.app.model.Pane.Builder builder();
-    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.ActionList? getActions();
     method public java.util.List<java.lang.Object!> getRows();
     method public boolean isLoading();
-    method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
   }
 
   public static final class Pane.Builder {
     ctor public Pane.Builder();
     method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
     method public androidx.car.app.model.Pane build();
-    method public androidx.car.app.model.Pane.Builder clearRows();
     method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
     method public androidx.car.app.model.Pane.Builder setLoading(boolean);
   }
@@ -590,10 +564,9 @@
 
   public class Row implements androidx.car.app.model.Item {
     method public static androidx.car.app.model.Row.Builder builder();
-    method public int getFlags();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata getMetadata();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public int getRowImageType();
     method public java.util.List<androidx.car.app.model.CarText!> getTexts();
     method public androidx.car.app.model.CarText getTitle();
@@ -604,17 +577,12 @@
     field public static final int IMAGE_TYPE_ICON = 4; // 0x4
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
     field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
-    field public static final int ROW_FLAG_NONE = 1; // 0x1
-    field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
-    field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
   }
 
   public static final class Row.Builder {
     method public androidx.car.app.model.Row.Builder addText(CharSequence);
     method public androidx.car.app.model.Row build();
-    method public androidx.car.app.model.Row.Builder clearText();
     method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
-    method public androidx.car.app.model.Row.Builder setFlags(int);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
     method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
@@ -623,14 +591,19 @@
     method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
   }
 
+  public interface SearchListenerWrapper {
+    method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
+    method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+  }
+
   public final class SearchTemplate implements androidx.car.app.model.Template {
-    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.model.SearchTemplate.SearchListener);
     method public androidx.car.app.model.ActionStrip? getActionStrip();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public String? getInitialSearchText();
     method public androidx.car.app.model.ItemList? getItemList();
     method public String? getSearchHint();
-    method public androidx.car.app.host.SearchListenerWrapper getSearchListener();
+    method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
     method public boolean isLoading();
     method public boolean isShowKeyboardByDefault();
   }
@@ -646,6 +619,11 @@
     method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
   }
 
+  public static interface SearchTemplate.SearchListener {
+    method public void onSearchSubmitted(String);
+    method public void onSearchTextChanged(String);
+  }
+
   public class SectionedItemList {
     method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
     method public androidx.car.app.model.CarText getHeader();
@@ -654,7 +632,6 @@
 
   public interface Template {
     method public default void checkPermissions(android.content.Context);
-    method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
   }
 
   public final class TemplateInfo {
@@ -680,7 +657,7 @@
 
   public class Toggle {
     method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
-    method public androidx.car.app.host.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+    method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
     method public boolean isChecked();
   }
 
@@ -735,7 +712,6 @@
   public class RowConstraints {
     method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
     method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
-    method public int getFlagOverrides();
     method public int getMaxActionsExclusive();
     method public int getMaxTextLinesPerRow();
     method public boolean isImageAllowed();
@@ -753,7 +729,6 @@
   public static final class RowConstraints.Builder {
     method public androidx.car.app.model.constraints.RowConstraints build();
     method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
-    method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
@@ -794,15 +769,17 @@
 package androidx.car.app.navigation {
 
   public class NavigationManager {
+    method @MainThread public void clearNavigationManagerListener();
     method @MainThread public void navigationEnded();
     method @MainThread public void navigationStarted();
-    method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+    method @MainThread public void setNavigationManagerListener(androidx.car.app.navigation.NavigationManagerListener);
+    method @MainThread public void setNavigationManagerListener(java.util.concurrent.Executor, androidx.car.app.navigation.NavigationManagerListener);
     method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
   }
 
   public interface NavigationManagerListener {
     method public void onAutoDriveEnabled();
-    method public void stopNavigation();
+    method public void onStopNavigation();
   }
 
 }
@@ -864,7 +841,11 @@
     field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
     field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
     field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+    field public static final int TYPE_FERRY_BOAT_LEFT = 47; // 0x2f
+    field public static final int TYPE_FERRY_BOAT_RIGHT = 48; // 0x30
     field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+    field public static final int TYPE_FERRY_TRAIN_LEFT = 49; // 0x31
+    field public static final int TYPE_FERRY_TRAIN_RIGHT = 50; // 0x32
     field public static final int TYPE_FORK_LEFT = 25; // 0x19
     field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
     field public static final int TYPE_KEEP_LEFT = 3; // 0x3
@@ -885,12 +866,16 @@
     field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
     field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
     field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
-    field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+    field @Deprecated public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
-    field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_ROUNDABOUT_ENTER_CCW = 45; // 0x2d
+    field public static final int TYPE_ROUNDABOUT_ENTER_CW = 43; // 0x2b
+    field @Deprecated public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_ROUNDABOUT_EXIT_CCW = 46; // 0x2e
+    field public static final int TYPE_ROUNDABOUT_EXIT_CW = 44; // 0x2c
     field public static final int TYPE_STRAIGHT = 36; // 0x24
     field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
     field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
@@ -957,9 +942,9 @@
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
-    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
     method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
   }
 
@@ -978,9 +963,9 @@
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
-    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
     method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
   }
@@ -997,8 +982,8 @@
   public static final class RoutingInfo.Builder {
     method public androidx.car.app.navigation.model.RoutingInfo build();
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
-    method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
   }
 
@@ -1023,8 +1008,8 @@
   }
 
   public final class TravelEstimate {
-    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
-    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.ZonedDateTime);
     method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
     method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
     method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
@@ -1032,12 +1017,15 @@
     method public androidx.car.app.model.CarColor getRemainingDistanceColor();
     method public androidx.car.app.model.CarColor getRemainingTimeColor();
     method public long getRemainingTimeSeconds();
+    field public static final long REMAINING_TIME_UNKNOWN = -1L; // 0xffffffffffffffffL
   }
 
   public static final class TravelEstimate.Builder {
     method public androidx.car.app.navigation.model.TravelEstimate build();
     method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+    method @RequiresApi(26) public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTime(java.time.Duration);
     method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeSeconds(long);
   }
 
   public final class Trip {
@@ -1062,7 +1050,7 @@
     method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
     method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
     method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
-    method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.Trip.Builder setLoading(boolean);
   }
 
 }
@@ -1079,8 +1067,8 @@
     method public CharSequence? getContentTitle();
     method public android.app.PendingIntent? getDeleteIntent();
     method public int getImportance();
-    method public android.graphics.Bitmap? getLargeIconBitmap();
-    method public int getSmallIconResId();
+    method public android.graphics.Bitmap? getLargeIcon();
+    method @DrawableRes public int getSmallIcon();
     method public boolean isExtended();
     method public static boolean isExtended(android.app.Notification);
   }
@@ -1089,7 +1077,6 @@
     ctor public CarAppExtender.Builder();
     method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
     method public androidx.car.app.notification.CarAppExtender build();
-    method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
     method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
     method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
     method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
@@ -1120,10 +1107,6 @@
 
 package androidx.car.app.utils {
 
-  public interface Logger {
-    method public void log(String);
-  }
-
   public class ThreadUtils {
     method public static void checkMainThread();
     method public static void runOnMain(Runnable);
@@ -1131,3 +1114,13 @@
 
 }
 
+package androidx.car.app.versioning {
+
+  public class CarAppApiLevels {
+    field public static final int LATEST = 1; // 0x1
+    field public static final int LEVEL_1 = 1; // 0x1
+    field public static final int OLDEST = 1; // 0x1
+  }
+
+}
+
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 5b604f6..827dffa 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -1,6 +1,12 @@
 // Signature format: 4.0
 package androidx.car.app {
 
+  public final class AppInfo {
+    method public int getLatestCarAppApiLevel();
+    method public String getLibraryVersion();
+    method public int getMinCarAppApiLevel();
+  }
+
   public class AppManager {
     method public void invalidate();
     method public void setSurfaceListener(androidx.car.app.SurfaceListener?);
@@ -14,37 +20,22 @@
     field public static final String NAVIGATION_TEMPLATES = "androidx.car.app.NAVIGATION_TEMPLATES";
   }
 
-  public abstract class CarAppService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+  public abstract class CarAppService extends android.app.Service {
     ctor public CarAppService();
-    method @CallSuper public void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
-    method public void finish();
-    method public final androidx.car.app.CarContext getCarContext();
-    method public androidx.car.app.HostInfo? getHostInfo();
-    method public androidx.lifecycle.Lifecycle getLifecycle();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
-    method public void onCarAppFinished();
+    method @CallSuper public final void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
+    method public final androidx.car.app.Session? getCurrentSession();
+    method public final androidx.car.app.HostInfo? getHostInfo();
+    method @CallSuper public final android.os.IBinder? onBind(android.content.Intent);
     method public void onCarConfigurationChanged(android.content.res.Configuration);
-    method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
-    method public final void onDestroy();
+    method public abstract androidx.car.app.Session onCreateSession();
     method public void onNewIntent(android.content.Intent);
     method public final boolean onUnbind(android.content.Intent);
-  }
-
-  public class CarAppVersion {
-    method public boolean isGreaterOrEqualTo(androidx.car.app.CarAppVersion);
-    method public static androidx.car.app.CarAppVersion? of(String) throws androidx.car.app.MalformedVersionException;
-    field public static final androidx.car.app.CarAppVersion HANDSHAKE_MIN_VERSION;
-    field public static final androidx.car.app.CarAppVersion INSTANCE;
-  }
-
-  public enum CarAppVersion.ReleaseSuffix {
-    method public static androidx.car.app.CarAppVersion.ReleaseSuffix fromString(String);
-    enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_BETA;
-    enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_EAP;
+    field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
   }
 
   public class CarContext extends android.content.ContextWrapper {
     method public void finishCarApp();
+    method public int getCarAppApiLevel();
     method public Object getCarService(String);
     method public <T> T getCarService(Class<T!>);
     method public String getCarServiceName(Class<?>);
@@ -53,11 +44,11 @@
     method public void startCarApp(android.content.Intent);
     method public static void startCarApp(android.content.Intent, android.content.Intent);
     field public static final String ACTION_NAVIGATE = "androidx.car.app.action.NAVIGATE";
-    field public static final String APP_SERVICE = "app_manager";
+    field public static final String APP_SERVICE = "app";
     field public static final String CAR_SERVICE = "car";
-    field public static final String NAVIGATION_SERVICE = "navigation_manager";
-    field public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
-    field public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+    field public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra.START_CAR_APP_BINDER_KEY";
+    field public static final String NAVIGATION_SERVICE = "navigation";
+    field public static final String SCREEN_SERVICE = "screen";
   }
 
   public final class CarToast {
@@ -89,18 +80,16 @@
   }
 
   public class HostInfo {
-    ctor public HostInfo(String, int);
     method public String getPackageName();
     method public int getUid();
   }
 
-  public class MalformedVersionException extends java.lang.Exception {
-    ctor public MalformedVersionException(String?);
-    ctor public MalformedVersionException(String, Throwable);
-    ctor public MalformedVersionException(Throwable?);
+  public interface OnDoneCallback {
+    method public void onFailure(androidx.car.app.serialization.Bundleable);
+    method public void onSuccess(androidx.car.app.serialization.Bundleable?);
   }
 
-  public interface OnScreenResultCallback {
+  public interface OnScreenResultListener {
     method public void onScreenResult(Object?);
   }
 
@@ -111,25 +100,27 @@
     method public final androidx.lifecycle.Lifecycle getLifecycle();
     method public String? getMarker();
     method public final androidx.car.app.ScreenManager getScreenManager();
-    method public abstract androidx.car.app.model.Template getTemplate();
     method public final void invalidate();
+    method public abstract androidx.car.app.model.Template onGetTemplate();
     method public void setMarker(String?);
     method public void setResult(Object?);
-    field public static final String ROOT = "ROOT";
   }
 
   public class ScreenManager {
     method public androidx.car.app.Screen getTop();
     method public void pop();
     method public void popTo(String);
+    method public void popToRoot();
     method public void push(androidx.car.app.Screen);
-    method public void pushForResult(androidx.car.app.Screen, androidx.car.app.OnScreenResultCallback);
+    method public void pushForResult(androidx.car.app.Screen, androidx.car.app.OnScreenResultListener);
     method public void remove(androidx.car.app.Screen);
   }
 
-  public interface SearchListener {
-    method public void onSearchSubmitted(String);
-    method public void onSearchTextChanged(String);
+  public abstract class Session implements androidx.lifecycle.LifecycleOwner {
+    ctor public Session();
+    method public final androidx.car.app.CarContext getCarContext();
+    method public androidx.lifecycle.Lifecycle getLifecycle();
+    method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
   }
 
   public class SurfaceContainer {
@@ -153,37 +144,10 @@
 
 }
 
-package androidx.car.app.host {
+package androidx.car.app.annotations {
 
-  public interface OnCheckedChangeListenerWrapper {
-    method public void onCheckedChange(boolean, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface OnDoneCallback {
-    method public void onFailure(androidx.car.app.serialization.Bundleable);
-    method public void onSuccess(androidx.car.app.serialization.Bundleable?);
-  }
-
-  public interface OnItemVisibilityChangedListenerWrapper {
-    method public void onItemVisibilityChanged(int, int, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface OnSelectedListenerWrapper {
-    method public void onSelected(int, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface SearchListenerWrapper {
-    method public void onSearchSubmitted(String, androidx.car.app.host.OnDoneCallback);
-    method public void onSearchTextChanged(String, androidx.car.app.host.OnDoneCallback);
-  }
-
-}
-
-package androidx.car.app.host.model {
-
-  public interface OnClickListenerWrapper {
-    method public boolean isParkedOnly();
-    method public void onClick(androidx.car.app.host.OnDoneCallback);
+  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public @interface RequiresCarApi {
+    method public abstract int value();
   }
 
 }
@@ -194,7 +158,7 @@
     method public static androidx.car.app.model.Action.Builder builder();
     method public androidx.car.app.model.CarColor getBackgroundColor();
     method public androidx.car.app.model.CarIcon? getIcon();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public androidx.car.app.model.CarText? getTitle();
     method public int getType();
     method public boolean isStandard();
@@ -231,7 +195,6 @@
     ctor public ActionStrip.Builder();
     method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
     method public androidx.car.app.model.ActionStrip build();
-    method public androidx.car.app.model.ActionStrip.Builder clearActions();
   }
 
   public class CarColor {
@@ -273,8 +236,6 @@
     field public static final int TYPE_CUSTOM = 1; // 0x1
     field public static final int TYPE_ERROR = 6; // 0x6
     field public static final int TYPE_UNKNOWN = 0; // 0x0
-    field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
-    field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
   }
 
   public static final class CarIcon.Builder {
@@ -358,9 +319,9 @@
     method public static androidx.car.app.model.GridItem.Builder builder();
     method public androidx.car.app.model.CarIcon getImage();
     method public int getImageType();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public androidx.car.app.model.CarText? getText();
-    method public androidx.car.app.model.CarText? getTitle();
+    method public androidx.car.app.model.CarText getTitle();
     method public androidx.car.app.model.Toggle? getToggle();
     field public static final int IMAGE_TYPE_ICON = 1; // 0x1
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -372,7 +333,7 @@
     method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
     method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
     method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
-    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
     method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
   }
 
@@ -404,10 +365,9 @@
     method public static androidx.car.app.model.ItemList.Builder builder();
     method public java.util.List<java.lang.Object!> getItems();
     method public androidx.car.app.model.CarText? getNoItemsMessage();
-    method public androidx.car.app.host.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangeListener();
-    method public androidx.car.app.host.OnSelectedListenerWrapper? getOnSelectedListener();
+    method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+    method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
     method public int getSelectedIndex();
-    method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
   }
 
   public static final class ItemList.Builder {
@@ -416,8 +376,8 @@
     method public androidx.car.app.model.ItemList build();
     method public androidx.car.app.model.ItemList.Builder clearItems();
     method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
-    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
-    method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangedListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+    method public androidx.car.app.model.ItemList.Builder setOnSelectedListener(androidx.car.app.model.ItemList.OnSelectedListener?);
     method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
   }
 
@@ -449,7 +409,6 @@
   public static final class ListTemplate.Builder {
     method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
     method public androidx.car.app.model.ListTemplate build();
-    method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
     method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
     method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
@@ -459,7 +418,7 @@
 
   public final class MessageTemplate implements androidx.car.app.model.Template {
     method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
-    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.ActionList? getActions();
     method public androidx.car.app.model.CarText? getDebugMessage();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public androidx.car.app.model.CarIcon? getIcon();
@@ -491,23 +450,38 @@
     method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
   }
 
+  public interface OnCheckedChangeListenerWrapper {
+    method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
+  }
+
   public interface OnClickListener {
     method public void onClick();
   }
 
+  public interface OnClickListenerWrapper {
+    method public boolean isParkedOnly();
+    method public void onClick(androidx.car.app.OnDoneCallback);
+  }
+
+  public interface OnItemVisibilityChangedListenerWrapper {
+    method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
+  }
+
+  public interface OnSelectedListenerWrapper {
+    method public void onSelected(int, androidx.car.app.OnDoneCallback);
+  }
+
   public final class Pane {
     method public static androidx.car.app.model.Pane.Builder builder();
-    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.ActionList? getActions();
     method public java.util.List<java.lang.Object!> getRows();
     method public boolean isLoading();
-    method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
   }
 
   public static final class Pane.Builder {
     ctor public Pane.Builder();
     method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
     method public androidx.car.app.model.Pane build();
-    method public androidx.car.app.model.Pane.Builder clearRows();
     method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
     method public androidx.car.app.model.Pane.Builder setLoading(boolean);
   }
@@ -590,10 +564,9 @@
 
   public class Row implements androidx.car.app.model.Item {
     method public static androidx.car.app.model.Row.Builder builder();
-    method public int getFlags();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata getMetadata();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public int getRowImageType();
     method public java.util.List<androidx.car.app.model.CarText!> getTexts();
     method public androidx.car.app.model.CarText getTitle();
@@ -604,17 +577,12 @@
     field public static final int IMAGE_TYPE_ICON = 4; // 0x4
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
     field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
-    field public static final int ROW_FLAG_NONE = 1; // 0x1
-    field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
-    field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
   }
 
   public static final class Row.Builder {
     method public androidx.car.app.model.Row.Builder addText(CharSequence);
     method public androidx.car.app.model.Row build();
-    method public androidx.car.app.model.Row.Builder clearText();
     method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
-    method public androidx.car.app.model.Row.Builder setFlags(int);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
     method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
@@ -623,14 +591,19 @@
     method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
   }
 
+  public interface SearchListenerWrapper {
+    method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
+    method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+  }
+
   public final class SearchTemplate implements androidx.car.app.model.Template {
-    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.model.SearchTemplate.SearchListener);
     method public androidx.car.app.model.ActionStrip? getActionStrip();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public String? getInitialSearchText();
     method public androidx.car.app.model.ItemList? getItemList();
     method public String? getSearchHint();
-    method public androidx.car.app.host.SearchListenerWrapper getSearchListener();
+    method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
     method public boolean isLoading();
     method public boolean isShowKeyboardByDefault();
   }
@@ -646,6 +619,11 @@
     method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
   }
 
+  public static interface SearchTemplate.SearchListener {
+    method public void onSearchSubmitted(String);
+    method public void onSearchTextChanged(String);
+  }
+
   public class SectionedItemList {
     method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
     method public androidx.car.app.model.CarText getHeader();
@@ -654,7 +632,6 @@
 
   public interface Template {
     method public default void checkPermissions(android.content.Context);
-    method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
   }
 
   public final class TemplateInfo {
@@ -680,7 +657,7 @@
 
   public class Toggle {
     method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
-    method public androidx.car.app.host.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+    method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
     method public boolean isChecked();
   }
 
@@ -735,7 +712,6 @@
   public class RowConstraints {
     method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
     method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
-    method public int getFlagOverrides();
     method public int getMaxActionsExclusive();
     method public int getMaxTextLinesPerRow();
     method public boolean isImageAllowed();
@@ -753,7 +729,6 @@
   public static final class RowConstraints.Builder {
     method public androidx.car.app.model.constraints.RowConstraints build();
     method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
-    method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
@@ -794,15 +769,17 @@
 package androidx.car.app.navigation {
 
   public class NavigationManager {
+    method @MainThread public void clearNavigationManagerListener();
     method @MainThread public void navigationEnded();
     method @MainThread public void navigationStarted();
-    method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+    method @MainThread public void setNavigationManagerListener(androidx.car.app.navigation.NavigationManagerListener);
+    method @MainThread public void setNavigationManagerListener(java.util.concurrent.Executor, androidx.car.app.navigation.NavigationManagerListener);
     method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
   }
 
   public interface NavigationManagerListener {
     method public void onAutoDriveEnabled();
-    method public void stopNavigation();
+    method public void onStopNavigation();
   }
 
 }
@@ -864,7 +841,11 @@
     field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
     field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
     field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+    field public static final int TYPE_FERRY_BOAT_LEFT = 47; // 0x2f
+    field public static final int TYPE_FERRY_BOAT_RIGHT = 48; // 0x30
     field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+    field public static final int TYPE_FERRY_TRAIN_LEFT = 49; // 0x31
+    field public static final int TYPE_FERRY_TRAIN_RIGHT = 50; // 0x32
     field public static final int TYPE_FORK_LEFT = 25; // 0x19
     field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
     field public static final int TYPE_KEEP_LEFT = 3; // 0x3
@@ -885,12 +866,16 @@
     field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
     field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
     field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
-    field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+    field @Deprecated public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
-    field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_ROUNDABOUT_ENTER_CCW = 45; // 0x2d
+    field public static final int TYPE_ROUNDABOUT_ENTER_CW = 43; // 0x2b
+    field @Deprecated public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_ROUNDABOUT_EXIT_CCW = 46; // 0x2e
+    field public static final int TYPE_ROUNDABOUT_EXIT_CW = 44; // 0x2c
     field public static final int TYPE_STRAIGHT = 36; // 0x24
     field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
     field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
@@ -957,9 +942,9 @@
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
-    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
     method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
   }
 
@@ -978,9 +963,9 @@
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
-    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
     method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
   }
@@ -997,8 +982,8 @@
   public static final class RoutingInfo.Builder {
     method public androidx.car.app.navigation.model.RoutingInfo build();
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
-    method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
   }
 
@@ -1023,8 +1008,8 @@
   }
 
   public final class TravelEstimate {
-    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
-    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.ZonedDateTime);
     method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
     method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
     method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
@@ -1032,12 +1017,15 @@
     method public androidx.car.app.model.CarColor getRemainingDistanceColor();
     method public androidx.car.app.model.CarColor getRemainingTimeColor();
     method public long getRemainingTimeSeconds();
+    field public static final long REMAINING_TIME_UNKNOWN = -1L; // 0xffffffffffffffffL
   }
 
   public static final class TravelEstimate.Builder {
     method public androidx.car.app.navigation.model.TravelEstimate build();
     method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+    method @RequiresApi(26) public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTime(java.time.Duration);
     method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeSeconds(long);
   }
 
   public final class Trip {
@@ -1062,7 +1050,7 @@
     method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
     method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
     method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
-    method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.Trip.Builder setLoading(boolean);
   }
 
 }
@@ -1079,8 +1067,8 @@
     method public CharSequence? getContentTitle();
     method public android.app.PendingIntent? getDeleteIntent();
     method public int getImportance();
-    method public android.graphics.Bitmap? getLargeIconBitmap();
-    method public int getSmallIconResId();
+    method public android.graphics.Bitmap? getLargeIcon();
+    method @DrawableRes public int getSmallIcon();
     method public boolean isExtended();
     method public static boolean isExtended(android.app.Notification);
   }
@@ -1089,7 +1077,6 @@
     ctor public CarAppExtender.Builder();
     method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
     method public androidx.car.app.notification.CarAppExtender build();
-    method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
     method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
     method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
     method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
@@ -1120,10 +1107,6 @@
 
 package androidx.car.app.utils {
 
-  public interface Logger {
-    method public void log(String);
-  }
-
   public class ThreadUtils {
     method public static void checkMainThread();
     method public static void runOnMain(Runnable);
@@ -1131,3 +1114,13 @@
 
 }
 
+package androidx.car.app.versioning {
+
+  public class CarAppApiLevels {
+    field public static final int LATEST = 1; // 0x1
+    field public static final int LEVEL_1 = 1; // 0x1
+    field public static final int OLDEST = 1; // 0x1
+  }
+
+}
+
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 5b604f6..827dffa 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -1,6 +1,12 @@
 // Signature format: 4.0
 package androidx.car.app {
 
+  public final class AppInfo {
+    method public int getLatestCarAppApiLevel();
+    method public String getLibraryVersion();
+    method public int getMinCarAppApiLevel();
+  }
+
   public class AppManager {
     method public void invalidate();
     method public void setSurfaceListener(androidx.car.app.SurfaceListener?);
@@ -14,37 +20,22 @@
     field public static final String NAVIGATION_TEMPLATES = "androidx.car.app.NAVIGATION_TEMPLATES";
   }
 
-  public abstract class CarAppService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+  public abstract class CarAppService extends android.app.Service {
     ctor public CarAppService();
-    method @CallSuper public void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
-    method public void finish();
-    method public final androidx.car.app.CarContext getCarContext();
-    method public androidx.car.app.HostInfo? getHostInfo();
-    method public androidx.lifecycle.Lifecycle getLifecycle();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
-    method public void onCarAppFinished();
+    method @CallSuper public final void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
+    method public final androidx.car.app.Session? getCurrentSession();
+    method public final androidx.car.app.HostInfo? getHostInfo();
+    method @CallSuper public final android.os.IBinder? onBind(android.content.Intent);
     method public void onCarConfigurationChanged(android.content.res.Configuration);
-    method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
-    method public final void onDestroy();
+    method public abstract androidx.car.app.Session onCreateSession();
     method public void onNewIntent(android.content.Intent);
     method public final boolean onUnbind(android.content.Intent);
-  }
-
-  public class CarAppVersion {
-    method public boolean isGreaterOrEqualTo(androidx.car.app.CarAppVersion);
-    method public static androidx.car.app.CarAppVersion? of(String) throws androidx.car.app.MalformedVersionException;
-    field public static final androidx.car.app.CarAppVersion HANDSHAKE_MIN_VERSION;
-    field public static final androidx.car.app.CarAppVersion INSTANCE;
-  }
-
-  public enum CarAppVersion.ReleaseSuffix {
-    method public static androidx.car.app.CarAppVersion.ReleaseSuffix fromString(String);
-    enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_BETA;
-    enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_EAP;
+    field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
   }
 
   public class CarContext extends android.content.ContextWrapper {
     method public void finishCarApp();
+    method public int getCarAppApiLevel();
     method public Object getCarService(String);
     method public <T> T getCarService(Class<T!>);
     method public String getCarServiceName(Class<?>);
@@ -53,11 +44,11 @@
     method public void startCarApp(android.content.Intent);
     method public static void startCarApp(android.content.Intent, android.content.Intent);
     field public static final String ACTION_NAVIGATE = "androidx.car.app.action.NAVIGATE";
-    field public static final String APP_SERVICE = "app_manager";
+    field public static final String APP_SERVICE = "app";
     field public static final String CAR_SERVICE = "car";
-    field public static final String NAVIGATION_SERVICE = "navigation_manager";
-    field public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
-    field public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+    field public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra.START_CAR_APP_BINDER_KEY";
+    field public static final String NAVIGATION_SERVICE = "navigation";
+    field public static final String SCREEN_SERVICE = "screen";
   }
 
   public final class CarToast {
@@ -89,18 +80,16 @@
   }
 
   public class HostInfo {
-    ctor public HostInfo(String, int);
     method public String getPackageName();
     method public int getUid();
   }
 
-  public class MalformedVersionException extends java.lang.Exception {
-    ctor public MalformedVersionException(String?);
-    ctor public MalformedVersionException(String, Throwable);
-    ctor public MalformedVersionException(Throwable?);
+  public interface OnDoneCallback {
+    method public void onFailure(androidx.car.app.serialization.Bundleable);
+    method public void onSuccess(androidx.car.app.serialization.Bundleable?);
   }
 
-  public interface OnScreenResultCallback {
+  public interface OnScreenResultListener {
     method public void onScreenResult(Object?);
   }
 
@@ -111,25 +100,27 @@
     method public final androidx.lifecycle.Lifecycle getLifecycle();
     method public String? getMarker();
     method public final androidx.car.app.ScreenManager getScreenManager();
-    method public abstract androidx.car.app.model.Template getTemplate();
     method public final void invalidate();
+    method public abstract androidx.car.app.model.Template onGetTemplate();
     method public void setMarker(String?);
     method public void setResult(Object?);
-    field public static final String ROOT = "ROOT";
   }
 
   public class ScreenManager {
     method public androidx.car.app.Screen getTop();
     method public void pop();
     method public void popTo(String);
+    method public void popToRoot();
     method public void push(androidx.car.app.Screen);
-    method public void pushForResult(androidx.car.app.Screen, androidx.car.app.OnScreenResultCallback);
+    method public void pushForResult(androidx.car.app.Screen, androidx.car.app.OnScreenResultListener);
     method public void remove(androidx.car.app.Screen);
   }
 
-  public interface SearchListener {
-    method public void onSearchSubmitted(String);
-    method public void onSearchTextChanged(String);
+  public abstract class Session implements androidx.lifecycle.LifecycleOwner {
+    ctor public Session();
+    method public final androidx.car.app.CarContext getCarContext();
+    method public androidx.lifecycle.Lifecycle getLifecycle();
+    method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
   }
 
   public class SurfaceContainer {
@@ -153,37 +144,10 @@
 
 }
 
-package androidx.car.app.host {
+package androidx.car.app.annotations {
 
-  public interface OnCheckedChangeListenerWrapper {
-    method public void onCheckedChange(boolean, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface OnDoneCallback {
-    method public void onFailure(androidx.car.app.serialization.Bundleable);
-    method public void onSuccess(androidx.car.app.serialization.Bundleable?);
-  }
-
-  public interface OnItemVisibilityChangedListenerWrapper {
-    method public void onItemVisibilityChanged(int, int, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface OnSelectedListenerWrapper {
-    method public void onSelected(int, androidx.car.app.host.OnDoneCallback);
-  }
-
-  public interface SearchListenerWrapper {
-    method public void onSearchSubmitted(String, androidx.car.app.host.OnDoneCallback);
-    method public void onSearchTextChanged(String, androidx.car.app.host.OnDoneCallback);
-  }
-
-}
-
-package androidx.car.app.host.model {
-
-  public interface OnClickListenerWrapper {
-    method public boolean isParkedOnly();
-    method public void onClick(androidx.car.app.host.OnDoneCallback);
+  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public @interface RequiresCarApi {
+    method public abstract int value();
   }
 
 }
@@ -194,7 +158,7 @@
     method public static androidx.car.app.model.Action.Builder builder();
     method public androidx.car.app.model.CarColor getBackgroundColor();
     method public androidx.car.app.model.CarIcon? getIcon();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public androidx.car.app.model.CarText? getTitle();
     method public int getType();
     method public boolean isStandard();
@@ -231,7 +195,6 @@
     ctor public ActionStrip.Builder();
     method public androidx.car.app.model.ActionStrip.Builder addAction(androidx.car.app.model.Action);
     method public androidx.car.app.model.ActionStrip build();
-    method public androidx.car.app.model.ActionStrip.Builder clearActions();
   }
 
   public class CarColor {
@@ -273,8 +236,6 @@
     field public static final int TYPE_CUSTOM = 1; // 0x1
     field public static final int TYPE_ERROR = 6; // 0x6
     field public static final int TYPE_UNKNOWN = 0; // 0x0
-    field public static final int TYPE_WILLIAM_ALERT = 7; // 0x7
-    field public static final androidx.car.app.model.CarIcon WILLIAM_ALERT;
   }
 
   public static final class CarIcon.Builder {
@@ -358,9 +319,9 @@
     method public static androidx.car.app.model.GridItem.Builder builder();
     method public androidx.car.app.model.CarIcon getImage();
     method public int getImageType();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public androidx.car.app.model.CarText? getText();
-    method public androidx.car.app.model.CarText? getTitle();
+    method public androidx.car.app.model.CarText getTitle();
     method public androidx.car.app.model.Toggle? getToggle();
     field public static final int IMAGE_TYPE_ICON = 1; // 0x1
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -372,7 +333,7 @@
     method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
     method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
     method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
-    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+    method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
     method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
   }
 
@@ -404,10 +365,9 @@
     method public static androidx.car.app.model.ItemList.Builder builder();
     method public java.util.List<java.lang.Object!> getItems();
     method public androidx.car.app.model.CarText? getNoItemsMessage();
-    method public androidx.car.app.host.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangeListener();
-    method public androidx.car.app.host.OnSelectedListenerWrapper? getOnSelectedListener();
+    method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+    method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
     method public int getSelectedIndex();
-    method public boolean isRefresh(androidx.car.app.model.ItemList?, androidx.car.app.utils.Logger);
   }
 
   public static final class ItemList.Builder {
@@ -416,8 +376,8 @@
     method public androidx.car.app.model.ItemList build();
     method public androidx.car.app.model.ItemList.Builder clearItems();
     method public androidx.car.app.model.ItemList.Builder setNoItemsMessage(CharSequence?);
-    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangeListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
-    method public androidx.car.app.model.ItemList.Builder setSelectable(androidx.car.app.model.ItemList.OnSelectedListener?);
+    method public androidx.car.app.model.ItemList.Builder setOnItemsVisibilityChangedListener(androidx.car.app.model.ItemList.OnItemVisibilityChangedListener?);
+    method public androidx.car.app.model.ItemList.Builder setOnSelectedListener(androidx.car.app.model.ItemList.OnSelectedListener?);
     method public androidx.car.app.model.ItemList.Builder setSelectedIndex(int);
   }
 
@@ -449,7 +409,6 @@
   public static final class ListTemplate.Builder {
     method public androidx.car.app.model.ListTemplate.Builder addList(androidx.car.app.model.ItemList, CharSequence);
     method public androidx.car.app.model.ListTemplate build();
-    method public androidx.car.app.model.ListTemplate.Builder clearAllLists();
     method public androidx.car.app.model.ListTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.model.ListTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
     method public androidx.car.app.model.ListTemplate.Builder setLoading(boolean);
@@ -459,7 +418,7 @@
 
   public final class MessageTemplate implements androidx.car.app.model.Template {
     method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
-    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.ActionList? getActions();
     method public androidx.car.app.model.CarText? getDebugMessage();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public androidx.car.app.model.CarIcon? getIcon();
@@ -491,23 +450,38 @@
     method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
   }
 
+  public interface OnCheckedChangeListenerWrapper {
+    method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
+  }
+
   public interface OnClickListener {
     method public void onClick();
   }
 
+  public interface OnClickListenerWrapper {
+    method public boolean isParkedOnly();
+    method public void onClick(androidx.car.app.OnDoneCallback);
+  }
+
+  public interface OnItemVisibilityChangedListenerWrapper {
+    method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
+  }
+
+  public interface OnSelectedListenerWrapper {
+    method public void onSelected(int, androidx.car.app.OnDoneCallback);
+  }
+
   public final class Pane {
     method public static androidx.car.app.model.Pane.Builder builder();
-    method public androidx.car.app.model.ActionList? getActionList();
+    method public androidx.car.app.model.ActionList? getActions();
     method public java.util.List<java.lang.Object!> getRows();
     method public boolean isLoading();
-    method public boolean isRefresh(androidx.car.app.model.Pane?, androidx.car.app.utils.Logger);
   }
 
   public static final class Pane.Builder {
     ctor public Pane.Builder();
     method public androidx.car.app.model.Pane.Builder addRow(androidx.car.app.model.Row);
     method public androidx.car.app.model.Pane build();
-    method public androidx.car.app.model.Pane.Builder clearRows();
     method public androidx.car.app.model.Pane.Builder setActions(java.util.List<androidx.car.app.model.Action!>);
     method public androidx.car.app.model.Pane.Builder setLoading(boolean);
   }
@@ -590,10 +564,9 @@
 
   public class Row implements androidx.car.app.model.Item {
     method public static androidx.car.app.model.Row.Builder builder();
-    method public int getFlags();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata getMetadata();
-    method public androidx.car.app.host.model.OnClickListenerWrapper? getOnClickListener();
+    method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
     method public int getRowImageType();
     method public java.util.List<androidx.car.app.model.CarText!> getTexts();
     method public androidx.car.app.model.CarText getTitle();
@@ -604,17 +577,12 @@
     field public static final int IMAGE_TYPE_ICON = 4; // 0x4
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
     field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
-    field public static final int ROW_FLAG_NONE = 1; // 0x1
-    field public static final int ROW_FLAG_SECTION_HEADER = 4; // 0x4
-    field public static final int ROW_FLAG_SHOW_DIVIDERS = 2; // 0x2
   }
 
   public static final class Row.Builder {
     method public androidx.car.app.model.Row.Builder addText(CharSequence);
     method public androidx.car.app.model.Row build();
-    method public androidx.car.app.model.Row.Builder clearText();
     method public androidx.car.app.model.Row.Builder setBrowsable(boolean);
-    method public androidx.car.app.model.Row.Builder setFlags(int);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon?, int);
     method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
@@ -623,14 +591,19 @@
     method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
   }
 
+  public interface SearchListenerWrapper {
+    method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
+    method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+  }
+
   public final class SearchTemplate implements androidx.car.app.model.Template {
-    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+    method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.model.SearchTemplate.SearchListener);
     method public androidx.car.app.model.ActionStrip? getActionStrip();
     method public androidx.car.app.model.Action? getHeaderAction();
     method public String? getInitialSearchText();
     method public androidx.car.app.model.ItemList? getItemList();
     method public String? getSearchHint();
-    method public androidx.car.app.host.SearchListenerWrapper getSearchListener();
+    method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
     method public boolean isLoading();
     method public boolean isShowKeyboardByDefault();
   }
@@ -646,6 +619,11 @@
     method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
   }
 
+  public static interface SearchTemplate.SearchListener {
+    method public void onSearchSubmitted(String);
+    method public void onSearchTextChanged(String);
+  }
+
   public class SectionedItemList {
     method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
     method public androidx.car.app.model.CarText getHeader();
@@ -654,7 +632,6 @@
 
   public interface Template {
     method public default void checkPermissions(android.content.Context);
-    method public default boolean isRefresh(androidx.car.app.model.Template, androidx.car.app.utils.Logger);
   }
 
   public final class TemplateInfo {
@@ -680,7 +657,7 @@
 
   public class Toggle {
     method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
-    method public androidx.car.app.host.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+    method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
     method public boolean isChecked();
   }
 
@@ -735,7 +712,6 @@
   public class RowConstraints {
     method public static androidx.car.app.model.constraints.RowConstraints.Builder builder();
     method public androidx.car.app.model.constraints.CarIconConstraints getCarIconConstraints();
-    method public int getFlagOverrides();
     method public int getMaxActionsExclusive();
     method public int getMaxTextLinesPerRow();
     method public boolean isImageAllowed();
@@ -753,7 +729,6 @@
   public static final class RowConstraints.Builder {
     method public androidx.car.app.model.constraints.RowConstraints build();
     method public androidx.car.app.model.constraints.RowConstraints.Builder setCarIconConstraints(androidx.car.app.model.constraints.CarIconConstraints);
-    method public androidx.car.app.model.constraints.RowConstraints.Builder setFlagOverrides(int);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setImageAllowed(boolean);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxActionsExclusive(int);
     method public androidx.car.app.model.constraints.RowConstraints.Builder setMaxTextLinesPerRow(int);
@@ -794,15 +769,17 @@
 package androidx.car.app.navigation {
 
   public class NavigationManager {
+    method @MainThread public void clearNavigationManagerListener();
     method @MainThread public void navigationEnded();
     method @MainThread public void navigationStarted();
-    method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+    method @MainThread public void setNavigationManagerListener(androidx.car.app.navigation.NavigationManagerListener);
+    method @MainThread public void setNavigationManagerListener(java.util.concurrent.Executor, androidx.car.app.navigation.NavigationManagerListener);
     method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
   }
 
   public interface NavigationManagerListener {
     method public void onAutoDriveEnabled();
-    method public void stopNavigation();
+    method public void onStopNavigation();
   }
 
 }
@@ -864,7 +841,11 @@
     field public static final int TYPE_DESTINATION_RIGHT = 42; // 0x2a
     field public static final int TYPE_DESTINATION_STRAIGHT = 40; // 0x28
     field public static final int TYPE_FERRY_BOAT = 37; // 0x25
+    field public static final int TYPE_FERRY_BOAT_LEFT = 47; // 0x2f
+    field public static final int TYPE_FERRY_BOAT_RIGHT = 48; // 0x30
     field public static final int TYPE_FERRY_TRAIN = 38; // 0x26
+    field public static final int TYPE_FERRY_TRAIN_LEFT = 49; // 0x31
+    field public static final int TYPE_FERRY_TRAIN_RIGHT = 50; // 0x32
     field public static final int TYPE_FORK_LEFT = 25; // 0x19
     field public static final int TYPE_FORK_RIGHT = 26; // 0x1a
     field public static final int TYPE_KEEP_LEFT = 3; // 0x3
@@ -885,12 +866,16 @@
     field public static final int TYPE_ON_RAMP_SLIGHT_RIGHT = 14; // 0xe
     field public static final int TYPE_ON_RAMP_U_TURN_LEFT = 19; // 0x13
     field public static final int TYPE_ON_RAMP_U_TURN_RIGHT = 20; // 0x14
-    field public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
+    field @Deprecated public static final int TYPE_ROUNDABOUT_ENTER = 30; // 0x1e
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW = 34; // 0x22
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE = 35; // 0x23
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW = 32; // 0x20
     field public static final int TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE = 33; // 0x21
-    field public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_ROUNDABOUT_ENTER_CCW = 45; // 0x2d
+    field public static final int TYPE_ROUNDABOUT_ENTER_CW = 43; // 0x2b
+    field @Deprecated public static final int TYPE_ROUNDABOUT_EXIT = 31; // 0x1f
+    field public static final int TYPE_ROUNDABOUT_EXIT_CCW = 46; // 0x2e
+    field public static final int TYPE_ROUNDABOUT_EXIT_CW = 44; // 0x2c
     field public static final int TYPE_STRAIGHT = 36; // 0x24
     field public static final int TYPE_TURN_NORMAL_LEFT = 7; // 0x7
     field public static final int TYPE_TURN_NORMAL_RIGHT = 8; // 0x8
@@ -957,9 +942,9 @@
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
-    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
     method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
   }
 
@@ -978,9 +963,9 @@
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
-    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
     method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+    method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
     method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
   }
@@ -997,8 +982,8 @@
   public static final class RoutingInfo.Builder {
     method public androidx.car.app.navigation.model.RoutingInfo build();
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
-    method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+    method public androidx.car.app.navigation.model.RoutingInfo.Builder setLoading(boolean);
     method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
   }
 
@@ -1023,8 +1008,8 @@
   }
 
   public final class TravelEstimate {
-    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
-    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
+    method public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, androidx.car.app.model.DateTimeWithZone);
+    method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate.Builder builder(androidx.car.app.model.Distance, java.time.ZonedDateTime);
     method public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, long, androidx.car.app.model.DateTimeWithZone);
     method @RequiresApi(26) public static androidx.car.app.navigation.model.TravelEstimate create(androidx.car.app.model.Distance, java.time.Duration, java.time.ZonedDateTime);
     method public androidx.car.app.model.DateTimeWithZone? getArrivalTimeAtDestination();
@@ -1032,12 +1017,15 @@
     method public androidx.car.app.model.CarColor getRemainingDistanceColor();
     method public androidx.car.app.model.CarColor getRemainingTimeColor();
     method public long getRemainingTimeSeconds();
+    field public static final long REMAINING_TIME_UNKNOWN = -1L; // 0xffffffffffffffffL
   }
 
   public static final class TravelEstimate.Builder {
     method public androidx.car.app.navigation.model.TravelEstimate build();
     method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingDistanceColor(androidx.car.app.model.CarColor);
+    method @RequiresApi(26) public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTime(java.time.Duration);
     method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeColor(androidx.car.app.model.CarColor);
+    method public androidx.car.app.navigation.model.TravelEstimate.Builder setRemainingTimeSeconds(long);
   }
 
   public final class Trip {
@@ -1062,7 +1050,7 @@
     method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
     method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
     method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
-    method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+    method public androidx.car.app.navigation.model.Trip.Builder setLoading(boolean);
   }
 
 }
@@ -1079,8 +1067,8 @@
     method public CharSequence? getContentTitle();
     method public android.app.PendingIntent? getDeleteIntent();
     method public int getImportance();
-    method public android.graphics.Bitmap? getLargeIconBitmap();
-    method public int getSmallIconResId();
+    method public android.graphics.Bitmap? getLargeIcon();
+    method @DrawableRes public int getSmallIcon();
     method public boolean isExtended();
     method public static boolean isExtended(android.app.Notification);
   }
@@ -1089,7 +1077,6 @@
     ctor public CarAppExtender.Builder();
     method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
     method public androidx.car.app.notification.CarAppExtender build();
-    method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
     method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
     method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
     method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
@@ -1120,10 +1107,6 @@
 
 package androidx.car.app.utils {
 
-  public interface Logger {
-    method public void log(String);
-  }
-
   public class ThreadUtils {
     method public static void checkMainThread();
     method public static void runOnMain(Runnable);
@@ -1131,3 +1114,13 @@
 
 }
 
+package androidx.car.app.versioning {
+
+  public class CarAppApiLevels {
+    field public static final int LATEST = 1; // 0x1
+    field public static final int LEVEL_1 = 1; // 0x1
+    field public static final int OLDEST = 1; // 0x1
+  }
+
+}
+
diff --git a/car/app/app/build.gradle b/car/app/app/build.gradle
index 5e853f9..d0811a6 100644
--- a/car/app/app/build.gradle
+++ b/car/app/app/build.gradle
@@ -31,21 +31,22 @@
     implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
     implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
 
-    androidTestImplementation(ANDROIDX_TEST_CORE)
-    androidTestImplementation(ANDROIDX_TEST_RULES)
-    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
-    androidTestImplementation(ANDROIDX_TEST_RUNNER)
     // TODO(shiufai): We need this for assertThrows. Point back to the AndroidX shared version if
     // it is ever upgraded.
-    androidTestImplementation("junit:junit:4.13")
-    androidTestImplementation(TRUTH)
-    androidTestImplementation(MOCKITO_ANDROID)
+    testImplementation("junit:junit:4.13")
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RUNNER)
+    testImplementation(JUNIT)
+    testImplementation(MOCKITO_CORE)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(TRUTH)
 }
 
 android {
     defaultConfig {
         minSdkVersion 21
         multiDexEnabled = true
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
     lintOptions {
         // We have a bunch of builder/inner classes where the outer classes access the private
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarAppVersionTest.java b/car/app/app/src/androidTest/java/androidx/car/app/CarAppVersionTest.java
deleted file mode 100644
index b4d28ee..0000000
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarAppVersionTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.car.app;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import androidx.car.app.CarAppVersion.ReleaseSuffix;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link CarAppVersion}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public final class CarAppVersionTest {
-
-    @Test
-    public void majorVersion() {
-        CarAppVersion hostVersion = CarAppVersion.create(2, 0, 0);
-        CarAppVersion clientVersion = CarAppVersion.create(1, 0, 0);
-
-        assertThat(hostVersion.isGreaterOrEqualTo(clientVersion)).isTrue();
-        assertThat(clientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
-    }
-
-    @Test
-    public void minorVersion() {
-        CarAppVersion hostVersion = CarAppVersion.create(2, 1, 0);
-        CarAppVersion clientVersion = CarAppVersion.create(2, 0, 0);
-
-        assertThat(hostVersion.isGreaterOrEqualTo(clientVersion)).isTrue();
-        assertThat(clientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
-    }
-
-    @Test
-    public void patchVersion() {
-        CarAppVersion hostVersion = CarAppVersion.create(3, 2, 1);
-        CarAppVersion clientVersion = CarAppVersion.create(3, 2, 0);
-
-        assertThat(hostVersion.isGreaterOrEqualTo(clientVersion)).isTrue();
-        assertThat(clientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
-    }
-
-    @Test
-    public void eapVersion_requiresExactMatch() {
-        CarAppVersion hostVersion = CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_EAP,
-                1);
-
-        CarAppVersion mismatchedClientVersion = CarAppVersion.create(4, 3, 2);
-        assertThat(hostVersion.isGreaterOrEqualTo(mismatchedClientVersion)).isFalse();
-        assertThat(mismatchedClientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
-
-        CarAppVersion matchedClientVersion =
-                CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_EAP, 1);
-        assertThat(hostVersion.isGreaterOrEqualTo(matchedClientVersion)).isTrue();
-        assertThat(matchedClientVersion.isGreaterOrEqualTo(hostVersion)).isTrue();
-    }
-
-    @Test
-    public void stableVersion_compatibleWithAllBeta() {
-        CarAppVersion hostVersion = CarAppVersion.create(3, 2, 1);
-
-        CarAppVersion clientVersion1 =
-                CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_BETA, 1);
-        assertThat(hostVersion.isGreaterOrEqualTo(clientVersion1)).isTrue();
-        assertThat(clientVersion1.isGreaterOrEqualTo(hostVersion)).isFalse();
-
-        CarAppVersion clientVersion2 =
-                CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_BETA, 2);
-        assertThat(hostVersion.isGreaterOrEqualTo(clientVersion2)).isTrue();
-        assertThat(clientVersion2.isGreaterOrEqualTo(hostVersion)).isFalse();
-    }
-
-    @Test
-    public void versionString_malformed_multipleHyphens() {
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap.4-5"));
-    }
-
-    @Test
-    public void versionString_malformed_mainVersionIncorrectNumbers() {
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1"));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1."));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2"));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2."));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3.4"));
-    }
-
-    @Test
-    public void versionString_malformed_invalidNumberFormat() {
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3c-eap.4"));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap.4c"));
-    }
-
-    @Test
-    public void versionString_malformed_incorrectReleaseSuffix() {
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-"));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap"));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap."));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap.4."));
-        assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eaP.4"));
-    }
-
-    @Test
-    public void versionString() throws MalformedVersionException {
-        String version1 = "1.2.3";
-        String version2 = "1.2.3-eap.0";
-        String version3 = "1.2.3-beta.1";
-
-        assertThat(CarAppVersion.of(version1).toString()).isEqualTo(version1);
-        assertThat(CarAppVersion.of(version2).toString()).isEqualTo(version2);
-        assertThat(CarAppVersion.of(version3).toString()).isEqualTo(version3);
-    }
-}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
deleted file mode 100644
index ef3832b..0000000
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.car.app.model;
-
-import static androidx.car.app.model.CarIcon.ALERT;
-import static androidx.car.app.model.CarIcon.BACK;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import androidx.car.app.TestUtils;
-import androidx.car.app.utils.Logger;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link GridTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class GridTemplateTest {
-    private final Logger mLogger = message -> {
-    };
-
-    @Test
-    public void createInstance_emptyList_notLoading_throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> GridTemplate.builder().setTitle("Title").build());
-
-        // Positive case
-        GridTemplate.builder().setTitle("Title").setLoading(true).build();
-    }
-
-    @Test
-    public void createInstance_isLoading_hasList_throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () ->
-                        GridTemplate.builder()
-                                .setTitle("Title")
-                                .setLoading(true)
-                                .setSingleList(TestUtils.getGridItemList(2))
-                                .build());
-    }
-
-    @Test
-    public void createInstance_noHeaderTitleOrAction_throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> GridTemplate.builder().setSingleList(TestUtils.getGridItemList(2)).build());
-
-        // Positive cases.
-        GridTemplate.builder().setTitle("Title").setSingleList(
-                TestUtils.getGridItemList(2)).build();
-        GridTemplate.builder()
-                .setHeaderAction(Action.BACK)
-                .setSingleList(TestUtils.getGridItemList(2))
-                .build();
-    }
-
-    @Test
-    public void createInstance_setSingleList() {
-        ItemList list = TestUtils.getGridItemList(2);
-        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-        assertThat(template.getSingleList()).isEqualTo(list);
-    }
-
-    @Test
-    public void createInstance_setHeaderAction_invalidActionThrows() {
-        assertThrows(
-                IllegalArgumentException.class,
-                () ->
-                        GridTemplate.builder()
-                                .setHeaderAction(
-                                        Action.builder().setTitle("Action").setOnClickListener(
-                                                () -> {
-                                                }).build()));
-    }
-
-    @Test
-    public void createInstance_setHeaderAction() {
-        GridTemplate template =
-                GridTemplate.builder()
-                        .setSingleList(TestUtils.getGridItemList(2))
-                        .setHeaderAction(Action.BACK)
-                        .build();
-        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
-    }
-
-    @Test
-    public void createInstance_setActionStrip() {
-        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
-        GridTemplate template =
-                GridTemplate.builder()
-                        .setSingleList(TestUtils.getGridItemList(2))
-                        .setTitle("Title")
-                        .setActionStrip(actionStrip)
-                        .build();
-        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
-    }
-
-    @Test
-    public void createInstance_setBackground() {
-        GridTemplate template =
-                GridTemplate.builder()
-                        .setTitle("Title")
-                        .setLoading(true)
-                        .setBackgroundImage(BACK)
-                        .build();
-        assertThat(template.getBackgroundImage()).isEqualTo(BACK);
-    }
-
-    @Test
-    public void validate_fromLoadingState_isRefresh() {
-        GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
-        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
-        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Going from loading state to new content is allowed.
-        assertThat(
-                template.isRefresh(
-                        GridTemplate.builder().setTitle("Title").setLoading(true).build(),
-                        mLogger))
-                .isTrue();
-    }
-
-    @Test
-    public void validate_mutableProperties_isRefresh() {
-        GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
-        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
-        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Ensure a template is a refresh of itself.
-        assertThat(template.isRefresh(template, mLogger)).isTrue();
-
-        // Allowed mutable states.
-        assertThat(
-                template.isRefresh(
-                        GridTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder()
-                                                .addItem(gridItem.setOnClickListener(() -> {
-                                                }).setImage(BACK).build())
-                                                .build())
-                                .setHeaderAction(Action.BACK)
-                                .setActionStrip(
-                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
-                                .build(),
-                        mLogger))
-                .isTrue();
-    }
-
-    @Test
-    public void validate_titleUpdate_isNotRefresh() {
-        ItemList list = ItemList.builder().build();
-        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Title updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        GridTemplate.builder().setSingleList(list).setTitle("Title2").build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_gridItemImageAndTextUpdates_isNotRefresh() {
-        GridItem.Builder gridItem =
-                GridItem.builder().setImage(BACK).setTitle("Title1").setText("Text1");
-        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
-        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Ensure a template is a refresh of itself.
-        assertThat(template.isRefresh(template, mLogger)).isTrue();
-
-        // Image updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        GridTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder().addItem(
-                                                gridItem.setImage(ALERT).build()).build())
-                                .build(),
-                        mLogger))
-                .isFalse();
-
-        // Text updates are disallowed
-        assertThat(
-                template.isRefresh(
-                        GridTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder().addItem(
-                                                gridItem.setTitle("Title2").build()).build())
-                                .build(),
-                        mLogger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        GridTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder().addItem(
-                                                gridItem.setText("Text2").build()).build())
-                                .build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_newGridItem_isNotRefresh() {
-        GridItem.Builder gridItem = GridItem.builder().setImage(BACK);
-        ItemList list = ItemList.builder().addItem(gridItem.build()).build();
-        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Additional grid items are disallowed.
-        assertThat(
-                template.isRefresh(
-                        GridTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder()
-                                                .addItem(gridItem.build())
-                                                .addItem(gridItem.build())
-                                                .build())
-                                .build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_toLoadingState_isNotRefresh() {
-        // Going from content to loading state is disallowed.
-        assertThat(
-                GridTemplate.builder()
-                        .setTitle("Title")
-                        .setLoading(true)
-                        .build()
-                        .isRefresh(
-                                GridTemplate.builder()
-                                        .setTitle("Title")
-                                        .setSingleList(ItemList.builder().build())
-                                        .build(),
-                                mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void resetList_clearsSingleList() {
-        GridTemplate.Builder builder =
-                GridTemplate.builder()
-                        .setSingleList(TestUtils.getGridItemList(2))
-                        .setHeaderAction(Action.BACK);
-
-        assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
-    }
-
-    @Test
-    public void equals() {
-        ItemList itemList = ItemList.builder().build();
-        String title = "title";
-        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
-
-        GridTemplate template =
-                GridTemplate.builder()
-                        .setSingleList(itemList)
-                        .setHeaderAction(Action.BACK)
-                        .setActionStrip(actionStrip)
-                        .setTitle(title)
-                        .build();
-
-        assertThat(template)
-                .isEqualTo(
-                        GridTemplate.builder()
-                                .setSingleList(itemList)
-                                .setHeaderAction(Action.BACK)
-                                .setActionStrip(actionStrip)
-                                .setTitle(title)
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentItemList() {
-        ItemList itemList = ItemList.builder().build();
-
-        GridTemplate template =
-                GridTemplate.builder().setTitle("Title").setSingleList(itemList).build();
-
-        assertThat(template)
-                .isNotEqualTo(
-                        GridTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder().addItem(
-                                                GridItem.builder().setImage(BACK).build()).build())
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentHeaderAction() {
-        ItemList itemList = ItemList.builder().build();
-
-        GridTemplate template =
-                GridTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
-
-        assertThat(template)
-                .isNotEqualTo(
-                        GridTemplate.builder()
-                                .setSingleList(itemList)
-                                .setHeaderAction(Action.APP_ICON)
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentTitle() {
-        ItemList itemList = ItemList.builder().build();
-        String title = "title";
-
-        GridTemplate template = GridTemplate.builder().setSingleList(itemList).setTitle(
-                title).build();
-
-        assertThat(template)
-                .isNotEqualTo(GridTemplate.builder().setSingleList(itemList).setTitle(
-                        "foo").build());
-    }
-
-    @Test
-    public void notEquals_differentActionStrip() {
-        ItemList itemList = ItemList.builder().build();
-        String title = "title";
-
-        GridTemplate template =
-                GridTemplate.builder()
-                        .setSingleList(itemList)
-                        .setTitle(title)
-                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
-                        .build();
-
-        assertThat(template)
-                .isNotEqualTo(
-                        GridTemplate.builder()
-                                .setSingleList(itemList)
-                                .setTitle(title)
-                                .setActionStrip(
-                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
-                                .build());
-    }
-}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
deleted file mode 100644
index 66c227e..0000000
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
+++ /dev/null
@@ -1,609 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.car.app.model;
-
-import static androidx.car.app.model.CarIcon.ALERT;
-import static androidx.car.app.model.CarIcon.BACK;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import android.os.RemoteException;
-import android.text.SpannableString;
-
-import androidx.car.app.IOnDoneCallback;
-import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.model.ItemList.OnItemVisibilityChangedListener;
-import androidx.car.app.model.ItemList.OnSelectedListener;
-import androidx.car.app.utils.Logger;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.Collections;
-
-/** Tests for {@link ItemListTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ItemListTest {
-    @Rule
-    public final MockitoRule mockito = MockitoJUnit.rule();
-
-    @Mock
-    private IOnDoneCallback.Stub mMockOnDoneCallback;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void createEmpty() {
-        ItemList list = ItemList.builder().build();
-        assertThat(list.getItems()).isEqualTo(Collections.emptyList());
-    }
-
-    @Test
-    public void createRows() {
-        Row row1 = Row.builder().setTitle("Row1").build();
-        Row row2 = Row.builder().setTitle("Row2").build();
-        ItemList list = ItemList.builder().addItem(row1).addItem(row2).build();
-
-        assertThat(list.getItems()).hasSize(2);
-        assertThat(list.getItems().get(0)).isEqualTo(row1);
-        assertThat(list.getItems().get(1)).isEqualTo(row2);
-    }
-
-    @Test
-    public void createGridItems() {
-        GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
-        GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
-        ItemList list = ItemList.builder().addItem(gridItem1).addItem(gridItem2).build();
-
-        assertThat(list.getItems()).containsExactly(gridItem1, gridItem2).inOrder();
-    }
-
-    @Test
-    public void clearRows() {
-        Row row1 = Row.builder().setTitle("Row1").build();
-        Row row2 = Row.builder().setTitle("Row2").build();
-        ItemList list = ItemList.builder().addItem(row1).addItem(row2).clearItems().build();
-
-        assertThat(list.getItems()).isEmpty();
-    }
-
-    @Test
-    public void clearGridItems() {
-        GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
-        GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
-        ItemList list = ItemList.builder().addItem(gridItem1).addItem(
-                gridItem2).clearItems().build();
-
-        assertThat(list.getItems()).isEmpty();
-    }
-
-    @Test
-    public void setSelectedable_emptyList_throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> ItemList.builder().setSelectable(selectedIndex -> {
-                }).build());
-    }
-
-    @Test
-    public void setSelectedIndex_greaterThanListSize_throws() {
-        Row row1 = Row.builder().setTitle("Row1").build();
-        assertThrows(
-                IllegalStateException.class,
-                () -> ItemList.builder()
-                        .addItem(row1)
-                        .setSelectable(selectedIndex -> {
-                        })
-                        .setSelectedIndex(2)
-                        .build());
-    }
-
-    @Test
-    @UiThreadTest
-    public void setSelectable() throws RemoteException {
-        OnSelectedListener mockListener = mock(OnSelectedListener.class);
-        ItemList itemList =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("title").build())
-                        .setSelectable(mockListener)
-                        .build();
-
-        OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
-
-
-        itemList.getOnSelectedListener().onSelected(0, onDoneCallback);
-        verify(mockListener).onSelected(eq(0));
-        verify(onDoneCallback).onSuccess(null);
-    }
-
-    @Test
-    public void setSelectable_disallowOnClickListenerInRows() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> ItemList.builder()
-                        .addItem(Row.builder().setTitle("foo").setOnClickListener(() -> {
-                        }).build())
-                        .setSelectable((index) -> {
-                        })
-                        .build());
-
-        // Positive test.
-        ItemList.builder()
-                .addItem(Row.builder().setTitle("foo").build())
-                .setSelectable((index) -> {
-                })
-                .build();
-    }
-
-    @Test
-    public void setSelectable_disallowToggleInRow() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> ItemList.builder()
-                        .addItem(Row.builder().setToggle(Toggle.builder(isChecked -> {
-                        }).build()).build())
-                        .setSelectable((index) -> {
-                        })
-                        .build());
-    }
-
-    @Test
-    @UiThreadTest
-    public void setOnItemVisibilityChangeListener_triggerListener() {
-        OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
-        ItemList list =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("1").build())
-                        .setOnItemsVisibilityChangeListener(listener)
-                        .build();
-
-        OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
-        list.getOnItemsVisibilityChangeListener().onItemVisibilityChanged(0, 1,
-                onDoneCallback);
-        ArgumentCaptor<Integer> startIndexCaptor = ArgumentCaptor.forClass(Integer.class);
-        ArgumentCaptor<Integer> endIndexCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(listener).onItemVisibilityChanged(startIndexCaptor.capture(),
-                endIndexCaptor.capture());
-        verify(onDoneCallback).onSuccess(null);
-        assertThat(startIndexCaptor.getValue()).isEqualTo(0);
-        assertThat(endIndexCaptor.getValue()).isEqualTo(1);
-    }
-
-    @Test
-    @UiThreadTest
-    public void setOnItemVisibilityChangeListener_triggerListenerWithFailure() {
-        OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
-        ItemList list =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("1").build())
-                        .setOnItemsVisibilityChangeListener(listener)
-                        .build();
-
-        String testExceptionMessage = "Test exception";
-        doThrow(new RuntimeException(testExceptionMessage)).when(listener).onItemVisibilityChanged(
-                0, 1);
-
-        OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
-        try {
-            list.getOnItemsVisibilityChangeListener().onItemVisibilityChanged(0, 1,
-                    onDoneCallback);
-        } catch (WrappedRuntimeException e) {
-            assertThat(e.getMessage()).contains(testExceptionMessage);
-        }
-
-        ArgumentCaptor<Integer> startIndexCaptor = ArgumentCaptor.forClass(Integer.class);
-        ArgumentCaptor<Integer> endIndexCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(listener).onItemVisibilityChanged(startIndexCaptor.capture(),
-                endIndexCaptor.capture());
-        verify(onDoneCallback).onFailure(any());
-        assertThat(startIndexCaptor.getValue()).isEqualTo(0);
-        assertThat(endIndexCaptor.getValue()).isEqualTo(1);
-    }
-
-    @Test
-    public void validateRows_isRefresh() {
-        Logger logger = message -> {
-        };
-        Row.Builder row = Row.builder().setTitle("Title1");
-        ItemList listWithRows = ItemList.builder().addItem(row.build()).build();
-
-        // Text updates are disallowed.
-        ItemList listWithDifferentTitle =
-                ItemList.builder().addItem(row.setTitle("Title2").build()).build();
-        ItemList listWithDifferentText =
-                ItemList.builder().addItem(row.addText("Text").build()).build();
-        assertThat(listWithDifferentTitle.isRefresh(listWithRows, logger)).isFalse();
-        assertThat(listWithDifferentText.isRefresh(listWithRows, logger)).isFalse();
-
-        // Additional rows are disallowed.
-        ItemList listWithTwoRows = ItemList.builder().addItem(row.build()).addItem(
-                row.build()).build();
-        assertThat(listWithTwoRows.isRefresh(listWithRows, logger)).isFalse();
-    }
-
-    @Test
-    public void validateGridItems_isRefresh() {
-        Logger logger = message -> {
-        };
-        GridItem.Builder gridItem = GridItem.builder().setImage(BACK).setTitle("Title1");
-        ItemList listWithGridItems = ItemList.builder().addItem(gridItem.build()).build();
-
-        // Text updates are disallowed.
-        ItemList listWithDifferentTitle =
-                ItemList.builder().addItem(gridItem.setTitle("Title2").build()).build();
-        ItemList listWithDifferentText =
-                ItemList.builder().addItem(gridItem.setText("Text").build()).build();
-        assertThat(listWithDifferentTitle.isRefresh(listWithGridItems, logger)).isFalse();
-        assertThat(listWithDifferentText.isRefresh(listWithGridItems, logger)).isFalse();
-
-        // Image updates are disallowed.
-        ItemList listWithDifferentImage =
-                ItemList.builder().addItem(gridItem.setImage(ALERT).build()).build();
-        assertThat(listWithDifferentImage.isRefresh(listWithGridItems, logger)).isFalse();
-
-        // Additional grid items are disallowed.
-        ItemList listWithTwoGridItems =
-                ItemList.builder().addItem(gridItem.build()).addItem(gridItem.build()).build();
-        assertThat(listWithTwoGridItems.isRefresh(listWithGridItems, logger)).isFalse();
-    }
-
-    @Test
-    public void validateRows_isRefresh_differentSpansAreIgnored() {
-        Logger logger = message -> {
-        };
-        SpannableString textWithDistanceSpan = new SpannableString("Text");
-        textWithDistanceSpan.setSpan(
-                DistanceSpan.create(Distance.create(1000, Distance.UNIT_KILOMETERS)),
-                /* start= */ 0,
-                /* end= */ 1,
-                /* flags= */ 0);
-        SpannableString textWithDurationSpan = new SpannableString("Text");
-        textWithDurationSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
-
-        ItemList list1 =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle(textWithDistanceSpan).addText(
-                                textWithDurationSpan).build())
-                        .build();
-        ItemList list2 =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle(textWithDurationSpan).addText(
-                                textWithDistanceSpan).build())
-                        .build();
-        ItemList list3 =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("Text2").addText("Text2").build())
-                        .build();
-
-        assertThat(list2.isRefresh(list1, logger)).isTrue();
-        assertThat(list3.isRefresh(list1, logger)).isFalse();
-    }
-
-    @Test
-    public void validateRows_isRefresh_differentToggleStatesAllowTextUpdates() {
-        Logger logger = message -> {
-        };
-        Toggle onToggle = Toggle.builder(isChecked -> {
-        }).setChecked(true).build();
-        Toggle offToggle = Toggle.builder(isChecked -> {
-        }).setChecked(false).build();
-
-        ItemList listWithOnToggle =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("Title1").setToggle(onToggle).build())
-                        .build();
-        ItemList listWithOffToggle =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("Title1").setToggle(offToggle).build())
-                        .build();
-        ItemList listWithoutToggle =
-                ItemList.builder().addItem(Row.builder().setTitle("Title2").build()).build();
-        ItemList listWithOffToggleDifferentText =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("Title2").addText("Text").setToggle(
-                                offToggle).build())
-                        .build();
-        ItemList listWithOnToggleDifferentText =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("Title2").setToggle(onToggle).build())
-                        .build();
-
-        // Going from toggle to no toggle is not a refresh.
-        assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
-
-        // Going from on toggle to off toggle, or vice versa, is always a refresh
-        assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentText, logger)).isTrue();
-        assertThat(listWithOffToggleDifferentText.isRefresh(listWithOnToggle, logger)).isTrue();
-
-        // If toggle state is the same, then text changes are not considered a refresh.
-        assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentText, logger)).isFalse();
-        assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentText, logger)).isFalse();
-    }
-
-    @Test
-    public void validateGridItems_isRefresh_differentToggleStatesAllowTextUpdates() {
-        Logger logger = message -> {
-        };
-        Toggle onToggle = Toggle.builder(isChecked -> {
-        }).setChecked(true).build();
-        Toggle offToggle = Toggle.builder(isChecked -> {
-        }).setChecked(false).build();
-
-        ItemList listWithOnToggle =
-                ItemList.builder()
-                        .addItem(
-                                GridItem.builder().setImage(BACK).setTitle("Title1").setToggle(
-                                        onToggle).build())
-                        .build();
-        ItemList listWithOffToggle =
-                ItemList.builder()
-                        .addItem(
-                                GridItem.builder().setImage(BACK).setTitle("Title1").setToggle(
-                                        offToggle).build())
-                        .build();
-        ItemList listWithoutToggle =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title2").build())
-                        .build();
-        ItemList listWithOffToggleDifferentText =
-                ItemList.builder()
-                        .addItem(
-                                GridItem.builder()
-                                        .setImage(BACK)
-                                        .setTitle("Title2")
-                                        .setText("Text")
-                                        .setToggle(offToggle)
-                                        .build())
-                        .build();
-        ItemList listWithOnToggleDifferentText =
-                ItemList.builder()
-                        .addItem(
-                                GridItem.builder().setImage(BACK).setTitle("Title2").setToggle(
-                                        onToggle).build())
-                        .build();
-
-        // Going from toggle to no toggle is not a refresh.
-        assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
-
-        // Going from on toggle to off toggle, or vice versa, is always a refresh
-        assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentText, logger)).isTrue();
-        assertThat(listWithOffToggleDifferentText.isRefresh(listWithOnToggle, logger)).isTrue();
-
-        // If toggle state is the same, then text changes are not considered a refresh.
-        assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentText, logger)).isFalse();
-        assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentText, logger)).isFalse();
-    }
-
-    @Test
-    public void validateGridItems_isRefresh_differentToggleStatesAllowImageUpdates() {
-        Logger logger = message -> {
-        };
-        Toggle onToggle = Toggle.builder(isChecked -> {
-        }).setChecked(true).build();
-        Toggle offToggle = Toggle.builder(isChecked -> {
-        }).setChecked(false).build();
-
-        ItemList listWithOnToggle =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(BACK).setToggle(onToggle).build())
-                        .build();
-        ItemList listWithOffToggle =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(BACK).setToggle(offToggle).build())
-                        .build();
-        ItemList listWithoutToggle =
-                ItemList.builder().addItem(GridItem.builder().setImage(ALERT).build()).build();
-        ItemList listWithOffToggleDifferentImage =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(ALERT).setToggle(offToggle).build())
-                        .build();
-        ItemList listWithOnToggleDifferentImage =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(ALERT).setToggle(onToggle).build())
-                        .build();
-
-        // Going from toggle to no toggle is not a refresh.
-        assertThat(listWithOnToggle.isRefresh(listWithoutToggle, logger)).isFalse();
-
-        // Going from on toggle to off toggle, or vice versa, is always a refresh
-        assertThat(listWithOnToggle.isRefresh(listWithOffToggleDifferentImage, logger)).isTrue();
-        assertThat(listWithOffToggleDifferentImage.isRefresh(listWithOnToggle, logger)).isTrue();
-
-        // If toggle state is the same, then image changes are not considered a refresh.
-        assertThat(listWithOnToggle.isRefresh(listWithOnToggleDifferentImage, logger)).isFalse();
-        assertThat(listWithOffToggle.isRefresh(listWithOffToggleDifferentImage, logger)).isFalse();
-    }
-
-    @Test
-    public void validateGridItems_isRefresh_differentSelectedIndexAllowTextUpdates() {
-        Logger logger = message -> {
-        };
-        OnSelectedListener onSelectedListener = mock(OnSelectedListener.class);
-
-        ItemList listWithItem0Selected =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title11").build())
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title12").build())
-                        .setSelectable(onSelectedListener)
-                        .setSelectedIndex(0)
-                        .build();
-        ItemList listWithItem1Selected =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
-                        .setSelectable(onSelectedListener)
-                        .setSelectedIndex(1)
-                        .build();
-        ItemList listWithItem0SelectedDifferentText =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
-                        .setSelectable(onSelectedListener)
-                        .setSelectedIndex(0)
-                        .build();
-        ItemList listWithoutOnSelectedListener =
-                ItemList.builder()
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title21").build())
-                        .addItem(GridItem.builder().setImage(BACK).setTitle("Title22").build())
-                        .build();
-
-        // Selecting item 1 from item 0, or vice versa, is always a refresh.
-        assertThat(listWithItem0Selected.isRefresh(listWithItem1Selected, logger)).isTrue();
-        assertThat(listWithItem1Selected.isRefresh(listWithItem0Selected, logger)).isTrue();
-
-        // If item selection is the same, it is not considered a refresh
-        assertThat(listWithItem0Selected.isRefresh(listWithItem0SelectedDifferentText, logger))
-                .isFalse();
-
-        // If one of the ItemList doesn't have a selectable state, it is not a refresh.
-        assertThat(
-                listWithItem0Selected.isRefresh(listWithoutOnSelectedListener, logger)).isFalse();
-    }
-
-    @Test
-    public void equals_itemListWithRows() {
-        Row row = Row.builder().setTitle("Title").build();
-        ItemList itemList =
-                ItemList.builder()
-                        .setSelectable((index) -> {
-                        })
-                        .setNoItemsMessage("no items")
-                        .setSelectedIndex(0)
-                        .setOnItemsVisibilityChangeListener((start, end) -> {
-                        })
-                        .addItem(row)
-                        .build();
-        assertThat(itemList)
-                .isEqualTo(
-                        ItemList.builder()
-                                .setSelectable((index) -> {
-                                })
-                                .setNoItemsMessage("no items")
-                                .setSelectedIndex(0)
-                                .setOnItemsVisibilityChangeListener((start, end) -> {
-                                })
-                                .addItem(row)
-                                .build());
-    }
-
-    @Test
-    public void equals_itemListWithGridItems() {
-        GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
-        ItemList itemList =
-                ItemList.builder()
-                        .setSelectable((index) -> {
-                        })
-                        .setNoItemsMessage("no items")
-                        .setSelectedIndex(0)
-                        .setOnItemsVisibilityChangeListener((start, end) -> {
-                        })
-                        .addItem(gridItem)
-                        .build();
-        assertThat(itemList)
-                .isEqualTo(
-                        ItemList.builder()
-                                .setSelectable((index) -> {
-                                })
-                                .setNoItemsMessage("no items")
-                                .setSelectedIndex(0)
-                                .setOnItemsVisibilityChangeListener((start, end) -> {
-                                })
-                                .addItem(gridItem)
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentNoItemsMessage() {
-        ItemList itemList = ItemList.builder().setNoItemsMessage("no items").build();
-        assertThat(itemList).isNotEqualTo(ItemList.builder().setNoItemsMessage("YO").build());
-    }
-
-    @Test
-    public void notEquals_differentSelectedIndex() {
-        Row row = Row.builder().setTitle("Title").build();
-        ItemList itemList =
-                ItemList.builder().setSelectable((index) -> {
-                }).addItem(row).addItem(row).build();
-        assertThat(itemList)
-                .isNotEqualTo(
-                        ItemList.builder()
-                                .setSelectable((index) -> {
-                                })
-                                .setSelectedIndex(1)
-                                .addItem(row)
-                                .addItem(row)
-                                .build());
-    }
-
-    @Test
-    public void notEquals_missingSelectedListener() {
-        Row row = Row.builder().setTitle("Title").build();
-        ItemList itemList =
-                ItemList.builder().setSelectable((index) -> {
-                }).addItem(row).addItem(row).build();
-        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).addItem(row).build());
-    }
-
-    @Test
-    public void notEquals_missingVisibilityChangedListener() {
-        Row row = Row.builder().setTitle("Title").build();
-        ItemList itemList =
-                ItemList.builder()
-                        .setOnItemsVisibilityChangeListener((start, end) -> {
-                        })
-                        .addItem(row)
-                        .addItem(row)
-                        .build();
-        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).addItem(row).build());
-    }
-
-    @Test
-    public void notEquals_differentRows() {
-        Row row = Row.builder().setTitle("Title").build();
-        ItemList itemList = ItemList.builder().addItem(row).addItem(row).build();
-        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(row).build());
-    }
-
-    @Test
-    public void notEquals_differentGridItems() {
-        GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
-        ItemList itemList = ItemList.builder().addItem(gridItem).addItem(gridItem).build();
-        assertThat(itemList).isNotEqualTo(ItemList.builder().addItem(gridItem).build());
-    }
-}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
deleted file mode 100644
index fd05fa8c..0000000
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
+++ /dev/null
@@ -1,531 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.car.app.model;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.text.SpannableString;
-
-import androidx.car.app.test.R;
-import androidx.car.app.utils.Logger;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link ListTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ListTemplateTest {
-    private final Logger mLogger = message -> {
-    };
-
-    @Test
-    public void createInstance_emptyList_notLoading_Throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> ListTemplate.builder().setTitle("Title").build());
-
-        // Positive case
-        ListTemplate.builder().setTitle("Title").setLoading(true).build();
-    }
-
-    @Test
-    public void createInstance_isLoading_hasList_Throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () ->
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setLoading(true)
-                                .setSingleList(getList())
-                                .build());
-    }
-
-    @Test
-    public void addEmptyList_throws() {
-        ItemList emptyList = ItemList.builder().build();
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> ListTemplate.builder().setTitle("Title").addList(emptyList,
-                        "header").build());
-    }
-
-    @Test
-    public void addList_emptyHeader_throws() {
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> ListTemplate.builder().setTitle("Title").addList(getList(), "").build());
-    }
-
-    @Test
-    public void resetList_clearsSingleList() {
-        ListTemplate.Builder builder =
-                ListTemplate.builder().setTitle("Title").setSingleList(getList());
-        assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
-    }
-
-    @Test
-    public void resetList_clearsMultipleLists() {
-        ListTemplate.Builder builder =
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .addList(getList(), "header1")
-                        .addList(getList(), "header2");
-        assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
-    }
-
-    @Test
-    public void addList_withVisibilityListener_throws() {
-        ItemList list =
-                ItemList.builder()
-                        .addItem(Row.builder().setTitle("Title").build())
-                        .setOnItemsVisibilityChangeListener((start, end) -> {
-                        })
-                        .build();
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> ListTemplate.builder().setTitle("Title").addList(list, "header").build());
-    }
-
-    @Test
-    public void addList_moreThanMaxTexts_throws() {
-        Row rowExceedsMaxTexts =
-                Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
-                        "text3").build();
-        Row rowMeetingMaxTexts =
-                Row.builder().setTitle("Title").addText("text1").addText("text2").build();
-        assertThrows(
-                IllegalArgumentException.class,
-                () ->
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder().addItem(rowExceedsMaxTexts).build())
-                                .build());
-
-        // Positive case.
-        ListTemplate.builder()
-                .setTitle("Title")
-                .setSingleList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
-                .build();
-    }
-
-    @Test
-    public void createInstance_noHeaderTitleOrAction_throws() {
-        assertThrows(
-                IllegalStateException.class,
-                () -> ListTemplate.builder().setSingleList(getList()).build());
-
-        // Positive cases/.
-        ListTemplate.builder().setTitle("Title").setSingleList(getList()).build();
-        ListTemplate.builder().setHeaderAction(Action.BACK).setSingleList(getList()).build();
-    }
-
-    @Test
-    public void createInstance_setSingleList() {
-        ItemList list = getList();
-        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-        assertThat(template.getSingleList()).isEqualTo(list);
-        assertThat(template.getSectionLists()).isEmpty();
-    }
-
-    @Test
-    public void createInstance_addList() {
-        ItemList list1 = getList();
-        ItemList list2 = getList();
-        ListTemplate template =
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .addList(list1, "header1")
-                        .addList(list2, "header2")
-                        .build();
-        assertThat(template.getSingleList()).isNull();
-        assertThat(template.getSectionLists()).hasSize(2);
-        assertThat(template.getSectionLists().get(0).getItemList()).isEqualTo(list1);
-        assertThat(template.getSectionLists().get(0).getHeader().getText()).isEqualTo("header1");
-        assertThat(template.getSectionLists().get(1).getItemList()).isEqualTo(list2);
-        assertThat(template.getSectionLists().get(1).getHeader().getText()).isEqualTo("header2");
-    }
-
-    @Test
-    public void setSingleList_clearLists() {
-        ItemList list1 = getList();
-        ItemList list2 = getList();
-        ItemList list3 = getList();
-        ListTemplate template =
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .addList(list1, "header1")
-                        .addList(list2, "header2")
-                        .setSingleList(list3)
-                        .build();
-        assertThat(template.getSingleList()).isEqualTo(list3);
-        assertThat(template.getSectionLists()).isEmpty();
-    }
-
-    @Test
-    public void addList_clearSingleList() {
-        ItemList list1 = getList();
-        ItemList list2 = getList();
-        ItemList list3 = getList();
-        ListTemplate template =
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .setSingleList(list1)
-                        .addList(list2, "header1")
-                        .addList(list3, "header2")
-                        .build();
-        assertThat(template.getSingleList()).isNull();
-        assertThat(template.getSectionLists()).hasSize(2);
-    }
-
-    @Test
-    public void createInstance_setHeaderAction_invalidActionThrows() {
-        assertThrows(
-                IllegalArgumentException.class,
-                () ->
-                        ListTemplate.builder()
-                                .setHeaderAction(
-                                        Action.builder().setTitle("Action").setOnClickListener(
-                                                () -> {
-                                                }).build()));
-    }
-
-    @Test
-    public void createInstance_setHeaderAction() {
-        ListTemplate template =
-                ListTemplate.builder().setSingleList(getList()).setHeaderAction(
-                        Action.BACK).build();
-        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
-    }
-
-    @Test
-    public void createInstance_setActionStrip() {
-        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
-        ListTemplate template =
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .setSingleList(getList())
-                        .setActionStrip(actionStrip)
-                        .build();
-        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
-    }
-
-    @Test
-    public void validate_fromLoadingState_isRefresh() {
-        Row.Builder row = Row.builder().setTitle("Row1");
-        ItemList list = ItemList.builder().addItem(row.build()).build();
-        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Going from loading state to new content is allowed.
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder().setTitle("Title").setLoading(true).build(),
-                        mLogger))
-                .isTrue();
-    }
-
-    @Test
-    public void validate_mutableProperties_isRefresh() {
-        Row.Builder row = Row.builder().setTitle("Row1");
-        ItemList list = ItemList.builder().addItem(row.build()).build();
-        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Ensure a template is a refresh of itself.
-        assertThat(template.isRefresh(template, mLogger)).isTrue();
-
-        // Allowed mutable states.
-        SpannableString stringWithSpan = new SpannableString("Row1");
-        stringWithSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
-        IconCompat icon = IconCompat.createWithResource(
-                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder()
-                                                .addItem(
-                                                        row.setOnClickListener(() -> {
-                                                        })
-                                                                .setBrowsable(true)
-                                                                .setTitle(stringWithSpan)
-                                                                .setImage(CarIcon.of(icon))
-                                                                .build())
-                                                .build())
-                                .setHeaderAction(Action.BACK)
-                                .setActionStrip(
-                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
-                                .build(),
-                        mLogger))
-                .isTrue();
-    }
-
-    @Test
-    public void validate_multipleListMutableProperties_isRefresh() {
-        Row row = Row.builder().setTitle("Row1").build();
-        Row refreshRow =
-                Row.builder()
-                        .setTitle("Row1")
-                        .setOnClickListener(() -> {
-                        })
-                        .setBrowsable(true)
-                        .setImage(
-                                CarIcon.of(
-                                        IconCompat.createWithResource(
-                                                ApplicationProvider.getApplicationContext(),
-                                                R.drawable.ic_test_1)))
-                        .build();
-        ItemList list = ItemList.builder().addItem(row).build();
-        ListTemplate template =
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .addList(list, "header1")
-                        .addList(list, "header2")
-                        .build();
-
-        // Sublist refreshes are allowed as long as headers and  number of sections remain the same.
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .addList(ItemList.builder().addItem(refreshRow).build(), "header1")
-                                .addList(ItemList.builder().addItem(refreshRow).build(), "header2")
-                                .build(),
-                        mLogger))
-                .isTrue();
-    }
-
-    @Test
-    public void validate_titleUpdate_isNotRefresh() {
-        ItemList list = ItemList.builder().build();
-        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Title updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder().setSingleList(list).setTitle("Title2").build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_rowTextUpdate_isNotRefresh() {
-        Row.Builder row = Row.builder().setTitle("Row1");
-        ItemList list = ItemList.builder().addItem(row.build()).build();
-        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Text updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(ItemList.builder().addItem(
-                                        row.setTitle("Row2").build()).build())
-                                .build(),
-                        mLogger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(ItemList.builder().addItem(
-                                        row.addText("Text").build()).build())
-                                .build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_newRow_isNotRefresh() {
-        Row.Builder row = Row.builder().setTitle("Row1");
-        ItemList list = ItemList.builder().addItem(row.build()).build();
-        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Additional rows are disallowed.
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder().addItem(row.build()).addItem(
-                                                row.build()).build())
-                                .build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_multipleList_headerChange_isNotRefresh() {
-        Row.Builder row = Row.builder().setTitle("Row1");
-        ItemList list = ItemList.builder().addItem(row.build()).build();
-        ListTemplate template =
-                ListTemplate.builder().setTitle("Title").addList(list, "header1").build();
-
-        // Addition of lists are disallowed.
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder().setTitle("Title").addList(list, "header2").build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_newList_isNotRefresh() {
-        Row.Builder row = Row.builder().setTitle("Row1");
-        ItemList list = ItemList.builder().addItem(row.build()).build();
-        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
-                list).build();
-
-        // Addition of lists are disallowed.
-        assertThat(
-                template.isRefresh(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .addList(list, "header1")
-                                .addList(list, "header2")
-                                .build(),
-                        mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void validate_toLoadingState_isNotRefresh() {
-        // Going from content to loading state is disallowed.
-        assertThat(
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .setLoading(true)
-                        .build()
-                        .isRefresh(
-                                ListTemplate.builder()
-                                        .setTitle("Title")
-                                        .setSingleList(ItemList.builder().build())
-                                        .build(),
-                                mLogger))
-                .isFalse();
-    }
-
-    @Test
-    public void equals() {
-        ItemList itemList = ItemList.builder().build();
-        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
-        String title = "title";
-
-        ListTemplate template =
-                ListTemplate.builder()
-                        .setSingleList(itemList)
-                        .setActionStrip(actionStrip)
-                        .setHeaderAction(Action.BACK)
-                        .setTitle(title)
-                        .build();
-
-        assertThat(template)
-                .isEqualTo(
-                        ListTemplate.builder()
-                                .setSingleList(itemList)
-                                .setActionStrip(actionStrip)
-                                .setHeaderAction(Action.BACK)
-                                .setTitle(title)
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentItemList() {
-        ItemList itemList = ItemList.builder().build();
-
-        ListTemplate template =
-                ListTemplate.builder().setTitle("Title").setSingleList(itemList).build();
-
-        assertThat(template)
-                .isNotEqualTo(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(
-                                        ItemList.builder().addItem(
-                                                Row.builder().setTitle("Title").build()).build())
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentHeaderAction() {
-        ItemList itemList = ItemList.builder().build();
-
-        ListTemplate template =
-                ListTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
-
-        assertThat(template)
-                .isNotEqualTo(
-                        ListTemplate.builder()
-                                .setSingleList(itemList)
-                                .setHeaderAction(Action.APP_ICON)
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentActionStrip() {
-        ItemList itemList = ItemList.builder().build();
-        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
-
-        ListTemplate template =
-                ListTemplate.builder()
-                        .setTitle("Title")
-                        .setSingleList(itemList)
-                        .setActionStrip(actionStrip)
-                        .build();
-
-        assertThat(template)
-                .isNotEqualTo(
-                        ListTemplate.builder()
-                                .setTitle("Title")
-                                .setSingleList(itemList)
-                                .setActionStrip(
-                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
-                                .build());
-    }
-
-    @Test
-    public void notEquals_differentTitle() {
-        ItemList itemList = ItemList.builder().build();
-        String title = "title";
-
-        ListTemplate template = ListTemplate.builder().setSingleList(itemList).setTitle(
-                title).build();
-
-        assertThat(template)
-                .isNotEqualTo(ListTemplate.builder().setSingleList(itemList).setTitle(
-                        "yo").build());
-    }
-
-    private static ItemList getList() {
-        Row row1 = Row.builder().setTitle("Bananas").build();
-        Row row2 = Row.builder().setTitle("Oranges").build();
-        return ItemList.builder().addItem(row1).addItem(row2).build();
-    }
-}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl b/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl
index e8f3995..b7e4f99 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl
@@ -61,12 +61,12 @@
   void getManager(in String type, IOnDoneCallback callback) = 8;
 
   /**
-   * Requests the version string of the library used for building the app.
+   * Requests information of the application (min API level, target API level, etc.).
    */
-  void getCarAppVersion(IOnDoneCallback callback) = 9;
+  void getAppInfo(IOnDoneCallback callback) = 9;
 
   /**
-   * Sends host information to the app.
+   * Sends host information and negotiated API level to the app.
    */
   void onHandshakeCompleted(in Bundleable handshakeInfo, IOnDoneCallback callback) = 10;
 }
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnCheckedChangeListener.aidl
similarity index 95%
rename from car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnCheckedChangeListener.aidl
index 8383e12..46e758ed 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnCheckedChangeListener.aidl
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app;
+package androidx.car.app.model;
 
 import androidx.car.app.IOnDoneCallback;
 
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnItemVisibilityChangedListener.aidl
similarity index 96%
rename from car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnItemVisibilityChangedListener.aidl
index ff43f3f..451a2b0 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnItemVisibilityChangedListener.aidl
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app;
+package androidx.car.app.model;
 
 import androidx.car.app.IOnDoneCallback;
 
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnSelectedListener.aidl
similarity index 95%
rename from car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnSelectedListener.aidl
index 2896a83..2ee9f5f 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnSelectedListener.aidl
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app;
+package androidx.car.app.model;
 
 import androidx.car.app.IOnDoneCallback;
 
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/ISearchListener.aidl
similarity index 96%
rename from car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/ISearchListener.aidl
index 9af81e0..990f255 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/ISearchListener.aidl
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app;
+package androidx.car.app.model;
 
 import androidx.car.app.IOnDoneCallback;
 
diff --git a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
index 4e16e7e..7a3a01d 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
@@ -28,5 +28,5 @@
   * <p>The app should stop any audio guidance, routing notifications tagged for
   * the car, and metadata state updates.
   */
-  void stopNavigation(IOnDoneCallback callback) = 1;
+  void onStopNavigation(IOnDoneCallback callback) = 1;
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/AppInfo.java b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
new file mode 100644
index 0000000..4c52330
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app;
+
+import static androidx.car.app.utils.CommonUtils.TAG;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.versioning.CarAppApiLevel;
+import androidx.car.app.versioning.CarAppApiLevels;
+
+/**
+ * Container class for information about the app the host is connected to.
+ * <p>
+ * Hosts will use this information to provide the right level of compatibility, based on the
+ * application's minimum and maximum API level and its own set of supported API levels.
+ * <p>
+ * The application minimum API level is defined in the application's manifest using the
+ * following declaration.
+ * <pre>{@code
+ * <manifest ...>
+ *   <application ...>
+ *     <meta-data
+ *         android:name="androidx.car.app.min-api-level"
+ *         android:value="2" />
+ *     ...
+ *   </application>
+ * </manifest>
+ * }</pre>
+ * <p>
+ * @see CarContext#getCarAppApiLevel()
+ */
+public final class AppInfo {
+    // TODO(b/174803562): Automatically update the this version using Gradle
+    private static final String LIBRARY_VERSION = "1.0.0-alpha01";
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
+    @VisibleForTesting
+    public static final String MIN_API_LEVEL_MANIFEST_KEY = "androidx.car.app.min-api-level";
+
+    @Nullable
+    private final String mLibraryVersion;
+    @CarAppApiLevel
+    private final int mMinCarAppApiLevel;
+    @CarAppApiLevel
+    private final int mLatestCarAppApiLevel;
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
+    @NonNull
+    public static AppInfo create(@NonNull Context context) {
+        @CarAppApiLevel
+        int minApiLevel = retrieveMinCarAppApiLevel(context);
+        if (minApiLevel < CarAppApiLevels.OLDEST || minApiLevel > CarAppApiLevels.LATEST) {
+            throw new IllegalArgumentException("Min API level (" + MIN_API_LEVEL_MANIFEST_KEY
+                    + "=" + minApiLevel + ") is out of range (" + CarAppApiLevels.OLDEST + "-"
+                    + CarAppApiLevels.LATEST + ")");
+        }
+        return new AppInfo(minApiLevel, CarAppApiLevels.LATEST, LIBRARY_VERSION);
+    }
+
+    // Used for serialization
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
+    public AppInfo() {
+        mMinCarAppApiLevel = 0;
+        mLibraryVersion = null;
+        mLatestCarAppApiLevel = 0;
+    }
+
+    // Used for testing
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
+    @VisibleForTesting
+    public AppInfo(@CarAppApiLevel int minCarAppApiLevel, @CarAppApiLevel int latestCarAppApiLevel,
+            @NonNull String libraryVersion) {
+        mMinCarAppApiLevel = minCarAppApiLevel;
+        mLibraryVersion = libraryVersion;
+        mLatestCarAppApiLevel = latestCarAppApiLevel;
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
+    @VisibleForTesting
+    @CarAppApiLevel
+    public static int retrieveMinCarAppApiLevel(@NonNull Context context) {
+        try {
+            ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(
+                    context.getPackageName(),
+                    PackageManager.GET_META_DATA);
+            if (applicationInfo.metaData == null) {
+                Log.i(TAG, "Min API level not found (" + MIN_API_LEVEL_MANIFEST_KEY + "). "
+                        + "Assuming min API level = " + CarAppApiLevels.LATEST);
+                return CarAppApiLevels.LATEST;
+            }
+            return applicationInfo.metaData.getInt(MIN_API_LEVEL_MANIFEST_KEY);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Unable to read min API level from manifest. Assuming "
+                            + CarAppApiLevels.LATEST, e);
+            return CarAppApiLevels.LATEST;
+        }
+    }
+
+    @NonNull
+    public String getLibraryVersion() {
+        return requireNonNull(mLibraryVersion);
+    }
+
+    @CarAppApiLevel
+    public int getMinCarAppApiLevel() {
+        return mMinCarAppApiLevel;
+    }
+
+    @CarAppApiLevel
+    public int getLatestCarAppApiLevel() {
+        return mLatestCarAppApiLevel;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/AppManager.java b/car/app/app/src/main/java/androidx/car/app/AppManager.java
index 314ba5e..1196b19 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppManager.java
@@ -71,7 +71,7 @@
 
     /**
      * Requests the current template to be invalidated, which eventually triggers a call to {@link
-     * Screen#getTemplate} to get the new template to display.
+     * Screen#onGetTemplate} to get the new template to display.
      *
      * @throws HostException if the remote call fails.
      */
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index ecfad77..a7f71d5 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -38,7 +38,6 @@
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.Lifecycle.Event;
 import androidx.lifecycle.Lifecycle.State;
-import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
 
 import java.io.FileDescriptor;
@@ -48,46 +47,64 @@
 /**
  * The base class for implementing a car app that runs in the car.
  *
+ * <h4>Service Declaration</h4>
+ *
+ * The app must extend the {@link CarAppService} to be bound by the car host. The service must also
+ * respond to {@link Intent} actions coming from the host, by adding an
+ * <code>intent-filter</code> to the service in the <code>AndroidManifest.xml</code> that handles
+ * the {@link #SERVICE_INTERFACE} action. For example:
+ *
+ * <pre>{@code
+ * <service
+ *   android:name=".YourAppService"
+ *   android:exported="true">
+ *   <intent-filter>
+ *     <action android:name="androidx.car.app.CarAppService" />
+ *   </intent-filter>
+ * </service>
+ * }</pre>
+ *
  * <h4>Accessing Location</h4>
  *
  * When the app is running in the car display, the system will not consider it as being in the
- * foreground, and hence it will considered in the background for the purpose of retrieving location
- * as described <a
+ * foreground, and hence it will be considered in the background for the purpose of retrieving
+ * location as described <a
  * href="https://developer.android.com/about/versions/10/privacy/changes#app-access-device
  * -location">here</a>.
  *
  * <p>To reliably get location for your car app, we recommended that you use a <a
  * href="https://developer.android.com/guide/components/services?#Types-of-services">foreground
- * service</a>.
+ * service</a>. Also note that accessing location may become unreliable when the phone is in the
+ * battery saver mode.
  */
-// This lint warning is triggered because this has a finish() API. Suppress because we are not
-// actually cleaning any held resources in that method.
-@SuppressWarnings("NotCloseable")
-public abstract class CarAppService extends Service implements LifecycleOwner {
+public abstract class CarAppService extends Service {
+    /**
+     * The {@link Intent} that must be declared as handled by the service.
+     */
+    public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
     private static final String TAG = "CarAppService";
     private static final String AUTO_DRIVE = "AUTO_DRIVE";
 
-    @SuppressWarnings({"argument.type.incompatible", "assignment.type.incompatible"})
-    private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
-
-    @SuppressWarnings("argument.type.incompatible")
-    private final CarContext mCarContext = CarContext.create(mRegistry);
+    @Nullable
+    private Session mCurrentSession;
 
     @Nullable
-    HostInfo mHostInfo;
+    private HostInfo mHostInfo;
+    @Nullable
+    private AppInfo mAppInfo;
 
     /**
      * Handles the host binding to this car app.
      *
      * <p>This method is final to ensure this car app's lifecycle is handled properly.
      *
-     * <p>Use {@link #onCreateScreen} and {@link #onNewIntent} instead to handle incoming {@link
+     * <p>Use {@link #onCreateSession()} and {@link #onNewIntent} instead to handle incoming {@link
      * Intent}s.
      */
     @Override
     @CallSuper
     @Nullable
-    public IBinder onBind(@NonNull Intent intent) {
+    public final IBinder onBind(@NonNull Intent intent) {
         return mBinder;
     }
 
@@ -95,28 +112,28 @@
      * Handles the host unbinding from this car app.
      *
      * <p>This method is final to ensure this car app's lifecycle is handled properly.
-     *
-     * <p>Use {@link #onCarAppFinished} instead.
      */
     @Override
     public final boolean onUnbind(@NonNull Intent intent) {
         Log.d(TAG, "onUnbind intent: " + intent);
         runOnMain(() -> {
-            // Stop the car app
-            mRegistry.handleLifecycleEvent(Event.ON_STOP);
+            // Destroy the session
+            if (mCurrentSession != null) {
+                CarContext carContext = mCurrentSession.getCarContext();
 
-            // Stop any active navigation
-            mCarContext.getCarService(NavigationManager.class).stopNavigation();
+                // Stop any active navigation
+                carContext.getCarService(NavigationManager.class).onStopNavigation();
 
-            // Destroy all screens in the stack
-            mCarContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
+                // Destroy all screens in the stack
+                carContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
 
-            // Remove binders to the host
-            mCarContext.resetHosts();
+                // Remove binders to the host
+                carContext.resetHosts();
 
-            // Notify the app that the host has unbinded so that it may treat it similar
-            // to destroy
-            onCarAppFinished();
+                ((LifecycleRegistry) mCurrentSession.getLifecycle()).handleLifecycleEvent(
+                        Event.ON_DESTROY);
+            }
+            mCurrentSession = null;
         });
 
         // Return true to request an onRebind call.  This means that the process will cache this
@@ -126,103 +143,30 @@
     }
 
     /**
-     * Handles the system destroying this {@link CarAppService}.
+     * Creates a new {@link Session} for the application.
      *
-     * <p>This method is final to ensure this car app's lifecycle is handled properly.
+     * <p>This method is invoked the first time the app is started, or if the previous
+     * {@link Session} instance has been destroyed and the system has not yet destroyed
+     * this service.
      *
-     * <p>Use a {@link androidx.lifecycle.LifecycleObserver} to observe this car app's {@link
-     * Lifecycle}.
-     *
-     * @see #getLifecycle
-     */
-    @Override
-    public final void onDestroy() {
-        Log.d(TAG, "onDestroy");
-        runOnMain(
-                () -> {
-                    mRegistry.handleLifecycleEvent(Event.ON_DESTROY);
-                });
-        super.onDestroy();
-        Log.d(TAG, "onDestroy completed");
-    }
-
-    /**
-     * Notifies that this car app has finished and should be treated as if it is destroyed.
-     *
-     * <p>The {@link Screen}s in the stack managed by the {@link ScreenManager} are now all
-     * destroyed and removed from the screen stack.
-     *
-     * <p>{@link #onCreateScreen} will be called if the user reopens the app before the system has
-     * destroyed it.
-     *
-     * <p>For the purposes of the app's lifecycle, you should perform similar destroy functions that
-     * you would when this instance's {@link Lifecycle} becomes {@link Lifecycle.State#DESTROYED}.
+     * <p>Once the method returns, {@link Session#onCreateScreen(Intent)} will be called on the
+     * {@link Session} returned.
      *
      * <p>Called by the system, do not call this method directly.
      *
-     * @see #getLifecycle
-     */
-    public void onCarAppFinished() {
-    }
-
-    /**
-     * Requests to finish the car app.
-     *
-     * <p>Call this when your app is done and should be closed.
-     *
-     * <p>At some point after this call {@link #onCarAppFinished} will be called, and eventually the
-     * system will destroy this {@link CarAppService}.
-     */
-    public void finish() {
-        mCarContext.finishCarApp();
-    }
-
-    /**
-     * Returns the {@link CarContext} for this car app.
-     *
-     * <p><b>The {@link CarContext} is not fully initialized until this car app's {@link
-     * Lifecycle.State} is at least {@link Lifecycle.State#CREATED}</b>
-     *
-     * @see #getLifecycle
-     */
-    @NonNull
-    public final CarContext getCarContext() {
-        return mCarContext;
-    }
-
-    /**
-     * Requests the first {@link Screen} for the application.
-     *
-     * <p>This method is invoked when this car app is first opened by the user.
-     *
-     * <p>Once the method returns, {@link Screen#getTemplate} will be called on the {@link Screen}
-     * returned, and the app will be displayed on the car screen.
-     *
-     * <p>To pre-seed a back stack, you can push {@link Screen}s onto the stack, via {@link
-     * ScreenManager#push} during this method call.
-     *
-     * <p>This method is invoked the first time the app is started, or if this {@link CarAppService}
-     * instance has been previously finished and the system has not yet destroyed this car app (See
-     * {@link #onCarAppFinished}).
-     *
-     * <p>Called by the system, do not call this method directly.
-     *
-     * @param intent the intent that was used to start this app. If the app was started with a
-     *               call to {@link CarContext#startCarApp}, this intent will be equal to the
-     *               intent passed to that method.
      * @see CarContext#startCarApp
      */
     @NonNull
-    public abstract Screen onCreateScreen(@NonNull Intent intent);
+    public abstract Session onCreateSession();
 
     /**
      * Notifies that the car app has received a new {@link Intent}.
      *
-     * <p>Once the method returns, {@link Screen#getTemplate} will be called on the {@link Screen}
+     * <p>Once the method returns, {@link Screen#onGetTemplate} will be called on the {@link Screen}
      * that is on top of the {@link Screen} stack managed by the {@link ScreenManager}, and the app
      * will be displayed on the car screen.
      *
-     * <p>In contrast to {@link #onCreateScreen}, this method is invoked when the app has already
+     * <p>In contrast to {@link #onCreateSession}, this method is invoked when the app has already
      * been launched and has not been finished.
      *
      * <p>Often used to update the current {@link Screen} or pushing a new one on the stack,
@@ -251,68 +195,21 @@
     public void onCarConfigurationChanged(@NonNull Configuration newConfiguration) {
     }
 
-    /**
-     * Returns the {@link CarAppService}'s {@link Lifecycle}.
-     *
-     * <p>Here are some of the ways you can use the car app's {@link Lifecycle}:
-     *
-     * <ul>
-     *   <li>Observe its {@link Lifecycle} by calling {@link Lifecycle#addObserver}. You can use the
-     *       {@link androidx.lifecycle.LifecycleObserver} to take specific actions whenever the
-     *       {@link Screen} receives different {@link Event}s.
-     *   <li>Use this {@link CarAppService} to observe {@link androidx.lifecycle.LiveData}s that
-     *       may drive the backing data for your application.
-     * </ul>
-     *
-     * <p>What each lifecycle related event means for a car app:
-     *
-     * <dl>
-     *   <dt>{@link Event#ON_CREATE}
-     *   <dd>The car app has just been launched, and this car app is being initialized. {@link
-     *       #onCreateScreen} will be called at a point after this call.
-     *   <dt>{@link #onCreateScreen}
-     *   <dd>The host is ready for this car app to create the first {@link Screen} so that it can
-     *       display its template.
-     *   <dt>{@link Event#ON_START}
-     *   <dd>The application is now visible in the car screen.
-     *   <dt>{@link Event#ON_RESUME}
-     *   <dd>The user can now interact with this application.
-     *   <dt>{@link Event#ON_PAUSE}
-     *   <dd>The user can no longer interact with this application.
-     *   <dt>{@link Event#ON_STOP}
-     *   <dd>The application is no longer visible.
-     *   <dt>{@link #onCarAppFinished}
-     *   <dd>Either this car app has requested to be finished (see {@link #finish}), or the host has
-     *       finished this car app. Unless this is a navigation app, after a period of time that the
-     *       app is no longer displaying in the car, the host may finish this car app.
-     *   <dt>{@link Event#ON_DESTROY}
-     *   <dd>The OS has now destroyed this {@link CarAppService} instance, and it is no longer
-     *       valid.
-     * </dl>
-     *
-     * <p>Listeners that are added in {@link Event#ON_START}, should be removed in {@link
-     * Event#ON_STOP}.
-     *
-     * <p>Listeners that are added in {@link Event#ON_CREATE} should be removed in {@link
-     * Event#ON_DESTROY}.
-     *
-     * @see androidx.lifecycle.LifecycleObserver
-     */
-    @NonNull
-    @Override
-    public Lifecycle getLifecycle() {
-        return mRegistry;
-    }
-
     @Override
     @CallSuper
-    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+    public final void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
             @Nullable String[] args) {
         super.dump(fd, writer, args);
 
         for (String arg : args) {
             if (AUTO_DRIVE.equals(arg)) {
-                runOnMain(mCarContext.getCarService(NavigationManager.class)::onAutoDriveEnabled);
+                Log.d(TAG, "Executing onAutoDriveEnabled");
+                runOnMain(() -> {
+                    if (mCurrentSession != null) {
+                        mCurrentSession.getCarContext().getCarService(
+                                NavigationManager.class).onAutoDriveEnabled();
+                    }
+                });
             }
         }
     }
@@ -323,10 +220,20 @@
      * @see HostInfo
      */
     @Nullable
-    public HostInfo getHostInfo() {
+    public final HostInfo getHostInfo() {
         return mHostInfo;
     }
 
+    /**
+     * Retrieves the current {@link Session} for this service.
+     *
+     * @see Session
+     */
+    @Nullable
+    public final Session getCurrentSession() {
+        return mCurrentSession;
+    }
+
     private final ICarApp.Stub mBinder =
             new ICarApp.Stub() {
                 // incompatible argument for parameter context of attachBaseContext.
@@ -343,17 +250,25 @@
                         IOnDoneCallback callback) {
                     Log.d(TAG, "onAppCreate intent: " + intent);
                     RemoteUtils.dispatchHostCall(() -> {
+                        if (mCurrentSession == null
+                                || mCurrentSession.getLifecycle().getCurrentState()
+                                == State.DESTROYED) {
+                            mCurrentSession = onCreateSession();
+                            mAppInfo = AppInfo.create(mCurrentSession.getCarContext());
+                        }
+
                         // CarContext is not set up until the base Context is attached. First
                         // thing we need to do here is attach the base Context, so that any usage of
                         // it works after this point.
-                        CarContext carContext = getCarContext();
+                        CarContext carContext = mCurrentSession.getCarContext();
                         carContext.attachBaseContext(CarAppService.this, configuration);
                         carContext.setCarHost(carHost);
 
                         // Whenever the host unbinds, the screens in the stack are destroyed.  If
                         // there is another bind, before the OS has destroyed this Service, then
                         // the stack will be empty, and we need to treat it as a new instance.
-                        LifecycleRegistry registry = (LifecycleRegistry) getLifecycle();
+                        LifecycleRegistry registry =
+                                (LifecycleRegistry) mCurrentSession.getLifecycle();
                         Lifecycle.State state = registry.getCurrentState();
                         int screenStackSize = carContext.getCarService(
                                 ScreenManager.class).getScreenStack().size();
@@ -364,7 +279,7 @@
                                     + ", stack size: " + screenStackSize);
                             registry.handleLifecycleEvent(Event.ON_CREATE);
                             carContext.getCarService(ScreenManager.class).push(
-                                    onCreateScreen(intent));
+                                    mCurrentSession.onCreateScreen(intent));
                         } else {
                             Log.d(TAG, "onAppCreate the app was already created");
                             onNewIntentInternal(intent);
@@ -376,29 +291,43 @@
                 @Override
                 public void onAppStart(IOnDoneCallback callback) {
                     RemoteUtils.dispatchHostCall(
-                            () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
-                                    Event.ON_START), callback, "onAppStart");
+                            () -> {
+                                checkSessionIsValid(mCurrentSession);
+                                ((LifecycleRegistry) mCurrentSession.getLifecycle())
+                                        .handleLifecycleEvent(Event.ON_START);
+                            }, callback,
+                            "onAppStart");
                 }
 
                 @Override
                 public void onAppResume(IOnDoneCallback callback) {
                     RemoteUtils.dispatchHostCall(
-                            () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
-                                    Event.ON_RESUME), callback, "onAppResume");
+                            () -> {
+                                checkSessionIsValid(mCurrentSession);
+                                ((LifecycleRegistry) mCurrentSession.getLifecycle())
+                                        .handleLifecycleEvent(Event.ON_RESUME);
+                            }, callback,
+                            "onAppResume");
                 }
 
                 @Override
                 public void onAppPause(IOnDoneCallback callback) {
                     RemoteUtils.dispatchHostCall(
-                            () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
-                                    Event.ON_PAUSE), callback, "onAppPause");
+                            () -> {
+                                checkSessionIsValid(mCurrentSession);
+                                ((LifecycleRegistry) mCurrentSession.getLifecycle())
+                                        .handleLifecycleEvent(Event.ON_PAUSE);
+                            }, callback, "onAppPause");
                 }
 
                 @Override
                 public void onAppStop(IOnDoneCallback callback) {
                     RemoteUtils.dispatchHostCall(
-                            () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
-                                    Event.ON_STOP), callback, "onAppStop");
+                            () -> {
+                                checkSessionIsValid(mCurrentSession);
+                                ((LifecycleRegistry) mCurrentSession.getLifecycle())
+                                        .handleLifecycleEvent(Event.ON_STOP);
+                            }, callback, "onAppStop");
                 }
 
                 @Override
@@ -424,14 +353,14 @@
                             RemoteUtils.sendSuccessResponse(
                                     callback,
                                     "getManager",
-                                    getCarContext().getCarService(
+                                    mCurrentSession.getCarContext().getCarService(
                                             AppManager.class).getIInterface());
                             return;
                         case CarContext.NAVIGATION_SERVICE:
                             RemoteUtils.sendSuccessResponse(
                                     callback,
                                     "getManager",
-                                    mCarContext.getCarService(
+                                    mCurrentSession.getCarContext().getCarService(
                                             NavigationManager.class).getIInterface());
                             return;
                         default:
@@ -443,9 +372,9 @@
                 }
 
                 @Override
-                public void getCarAppVersion(IOnDoneCallback callback) {
+                public void getAppInfo(IOnDoneCallback callback) {
                     RemoteUtils.sendSuccessResponse(
-                            callback, "getCarAppVersion", CarAppVersion.INSTANCE.toString());
+                            callback, "getAppInfo", mAppInfo);
                 }
 
                 @Override
@@ -457,10 +386,13 @@
                         String packageName = deserializedHandshakeInfo.getHostPackageName();
                         int uid = Binder.getCallingUid();
                         mHostInfo = new HostInfo(packageName, uid);
-                    } catch (BundlerException e) {
+                        mCurrentSession.getCarContext().onHandshakeComplete(
+                                deserializedHandshakeInfo);
+                        RemoteUtils.sendSuccessResponse(callback, "onHandshakeCompleted", null);
+                    } catch (BundlerException | IllegalArgumentException e) {
                         mHostInfo = null;
+                        RemoteUtils.sendFailureResponse(callback, "onHandshakeCompleted", e);
                     }
-                    RemoteUtils.sendSuccessResponse(callback, "onHandshakeCompleted", null);
                 }
 
                 // call to onNewIntent(android.content.Intent) not allowed on the given receiver.
@@ -480,8 +412,15 @@
                     ThreadUtils.checkMainThread();
                     Log.d(TAG, "onCarConfigurationChanged configuration: " + configuration);
 
-                    getCarContext().onCarConfigurationChanged(configuration);
-                    onCarConfigurationChanged(getCarContext().getResources().getConfiguration());
+                    mCurrentSession.getCarContext().onCarConfigurationChanged(configuration);
+                    onCarConfigurationChanged(
+                            mCurrentSession.getCarContext().getResources().getConfiguration());
                 }
             };
+
+    private static void checkSessionIsValid(Session session) {
+        if (session == null) {
+            throw new IllegalStateException("Null session found when non-null expected.");
+        }
+    }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppVersion.java b/car/app/app/src/main/java/androidx/car/app/CarAppVersion.java
deleted file mode 100644
index d23f7e1..0000000
--- a/car/app/app/src/main/java/androidx/car/app/CarAppVersion.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.car.app;
-
-import static androidx.car.app.CarAppVersion.ReleaseSuffix.RELEASE_SUFFIX_BETA;
-import static androidx.car.app.CarAppVersion.ReleaseSuffix.RELEASE_SUFFIX_EAP;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Locale;
-
-/**
- * A versioning class used for compatibility checks between the host and client.
- *
- * <p>The version scheme follows semantic versioning and is defined as:
- *
- * <pre>major.minor.patch[-releasesSuffix.build]<pre>
- *
- * where:
- *
- * <ul>
- *   <li>major version differences denote binary incompatibility (e.g. API removal)
- *   <li>minor version differences denote compatible API changes (e.g. API additional/deprecation)
- *   <li>patch version differences denote non-API altering internal changes (e.g. bug fixes)
- *   <li>releaseSuffix is{@code null} for stable versions but otherwise reserved for special-purpose
- *       EAP builds and one-off public betas. For cases where a release suffix is provided, the
- *       appended build number is used to differentiate versions within the same suffix category.
- *       (e.g. 1.0.0-eap.1 vs 1.0.0-eap.2).
- * </ul>
- */
-public class CarAppVersion {
-    private static final String MAIN_VERSION_FORMAT = "%d.%d.%d";
-    private static final String SUFFIX_VERSION_FORMAT = "-%s.%d";
-
-    public static final CarAppVersion INSTANCE =
-            CarAppVersion.create(1, 0, 0, RELEASE_SUFFIX_BETA, 1);
-
-    /** Min version of the SDK which supports handshake completed binder call. */
-    public static final CarAppVersion HANDSHAKE_MIN_VERSION =
-            CarAppVersion.create(1, 0, 0, RELEASE_SUFFIX_BETA, 1);
-
-    /** Different types of supported version suffixes. */
-    public enum ReleaseSuffix {
-        RELEASE_SUFFIX_EAP("eap"),
-        RELEASE_SUFFIX_BETA("beta");
-
-        private final String mValue;
-
-        ReleaseSuffix(String value) {
-            this.mValue = value;
-        }
-
-        /** Creates a {@link ReleaseSuffix} from the string standard representation as described
-         * in the {@link #toString()} method */
-        @NonNull
-        public static ReleaseSuffix fromString(@NonNull String value) {
-            switch (value) {
-                case "eap":
-                    return RELEASE_SUFFIX_EAP;
-                case "beta":
-                    return RELEASE_SUFFIX_BETA;
-                default:
-                    throw new IllegalArgumentException(value + " is not a valid release suffix");
-            }
-        }
-
-        @NonNull
-        @Override
-        public String toString() {
-            return mValue;
-        }
-    }
-
-    private final int mMajor;
-    private final int mMinor;
-    private final int mPatch;
-    @Nullable
-    private final ReleaseSuffix mReleaseSuffix;
-    private final int mBuild;
-
-    /** Creates a {@link CarAppVersion} without a release suffix. (e.g. 1.0.0) */
-    @NonNull
-    static CarAppVersion create(int major, int minor, int patch) {
-        return new CarAppVersion(major, minor, patch, null, 0);
-    }
-
-    /**
-     * Creates a {@link CarAppVersion} with a release suffix and build number. (e.g. 1.0.0-eap.0)
-     *
-     * <p>Note that if {@code releaseSuffix} is {@code null}, then {@code build} is ignored.
-     */
-    @NonNull
-    static CarAppVersion create(
-            int major, int minor, int patch, ReleaseSuffix releaseSuffix, int build) {
-        return new CarAppVersion(major, minor, patch, releaseSuffix, build);
-    }
-
-    /**
-     * Creates a {@link CarAppVersion} instance based on its string representation.
-     *
-     * <p>The string should be of the format major.minor.patch(-releaseSuffix.build). If the
-     * string is malformed or {@code null}, {@code null} will be returned.
-     */
-    @Nullable
-    public static CarAppVersion of(@NonNull String versionString) throws MalformedVersionException {
-        return parseVersionString(versionString);
-    }
-
-    private static CarAppVersion parseVersionString(String versionString)
-            throws MalformedVersionException {
-        String[] versionSplit = versionString.split("-", -1);
-        if (versionSplit.length > 2) {
-            throw new MalformedVersionException(
-                    "Malformed version string (more than 1 \"-\" detected): " + versionString);
-        }
-
-        String mainVersion = versionSplit[0];
-        String[] mainVersionSplit = mainVersion.split("\\.", -1);
-
-        // Main version should be formatted as major.minor.patch
-        if (mainVersionSplit.length != 3) {
-            throw new MalformedVersionException(
-                    "Malformed version string (invalid main version format): " + versionString);
-        }
-
-        int major;
-        int minor;
-        int patch;
-        ReleaseSuffix releaseSuffix = null;
-        int build = 0;
-
-        try {
-            major = Integer.parseInt(mainVersionSplit[0]);
-            minor = Integer.parseInt(mainVersionSplit[1]);
-            patch = Integer.parseInt(mainVersionSplit[2]);
-
-            String suffixVersion = versionSplit.length > 1 ? versionSplit[1] : null;
-            if (suffixVersion != null) {
-                String[] suffixVersionSplit = suffixVersion.split("\\.", -1);
-
-                // Release suffix should be formatted as releaseSuffix.build
-                if (suffixVersionSplit.length != 2) {
-                    throw new MalformedVersionException(
-                            "Malformed version string (invalid suffix version format): "
-                                    + versionString);
-                }
-
-                try {
-                    releaseSuffix = ReleaseSuffix.fromString(suffixVersionSplit[0]);
-                } catch (IllegalArgumentException e) {
-                    throw new MalformedVersionException(
-                            "Malformed version string (unsupported suffix): " + versionString, e);
-                }
-                build = Integer.parseInt(suffixVersionSplit[1]);
-            }
-
-            return new CarAppVersion(major, minor, patch, releaseSuffix, build);
-        } catch (NumberFormatException exception) {
-            throw new MalformedVersionException(
-                    "Malformed version string (unsupported characters): " + versionString,
-                    exception);
-        }
-    }
-
-    private CarAppVersion(
-            int major, int minor, int patch, @Nullable ReleaseSuffix releaseSuffix, int build) {
-        this.mMajor = major;
-        this.mMinor = minor;
-        this.mPatch = patch;
-        this.mReleaseSuffix = releaseSuffix;
-        this.mBuild = build;
-    }
-
-    /** Returns the human-readable format of this version. */
-    @NonNull
-    @Override
-    public String toString() {
-        String versionString = String.format(Locale.US, MAIN_VERSION_FORMAT, mMajor, mMinor,
-                mPatch);
-        if (mReleaseSuffix != null) {
-            versionString += String.format(Locale.US, SUFFIX_VERSION_FORMAT, mReleaseSuffix,
-                    mBuild);
-        }
-
-        return versionString;
-    }
-
-    /**
-     * Checks whether this {@link CarAppVersion} is greater than or equal to {@code other}, which is
-     * used to determine compatibility. Returns true if so, false otherwise.
-     *
-     * <p>The rules of comparison are as follow:
-     *
-     * <ul>
-     *   <li>If either versions are suffixed with {@link ReleaseSuffix#RELEASE_SUFFIX_EAP}, the
-     *   version strings have to be exact match to be considered compatible.
-     *   <li>The major version has to be greater or equal to be considered compatible.
-     *   <li>If major versions are equal, the minor version has to be greater or equal to be
-     *   considered compatible.
-     *   <li>If major.minor versions are equal, the patch version has to be greater or equal to
-     *   be considered compatible.
-     *   <li>If the major.minor.patch versions are equal, for stable versions release suffix
-     *   equals {@code null}, the instance is considered compatible with all
-     *   {@link ReleaseSuffix#RELEASE_SUFFIX_BETA} versions; for
-     *   {@link ReleaseSuffix#RELEASE_SUFFIX_BETA}, the instance is considered compatible iff the
-     *   other is a {@link ReleaseSuffix#RELEASE_SUFFIX_BETA} version and that the build is
-     *   greater or equal to the other version.
-     * </ul>
-     */
-    public boolean isGreaterOrEqualTo(@NonNull CarAppVersion other) {
-        // For EAP versions, we require an exact match.
-        if (mReleaseSuffix == RELEASE_SUFFIX_EAP || other.mReleaseSuffix == RELEASE_SUFFIX_EAP) {
-            return mMajor == other.mMajor
-                    && mMinor == other.mMinor
-                    && mPatch == other.mPatch
-                    && mReleaseSuffix == other.mReleaseSuffix
-                    && mBuild == other.mBuild;
-        }
-
-        int result = Integer.compare(mMajor, other.mMajor);
-        // Major version differs, return.
-        if (result != 0) {
-            return result == 1;
-        }
-
-        // Minor version differs, return.
-        result = Integer.compare(mMinor, other.mMinor);
-        if (result != 0) {
-            return result == 1;
-        }
-
-        // Patch version differs, return.
-        result = Integer.compare(mPatch, other.mPatch);
-        if (result != 0) {
-            return result == 1;
-        }
-
-        if (mReleaseSuffix == null) {
-            // A stable version (is considered compatible to any beta versions, the suffix
-            // version is
-            // ignored in those cases.
-            return true;
-        } else if (mReleaseSuffix == RELEASE_SUFFIX_BETA) {
-            // Compatible if beta build is greater than other's beta build.
-            if (other.mReleaseSuffix == RELEASE_SUFFIX_BETA) {
-                return mBuild >= other.mBuild;
-            } else {
-                // Beta version is incompatible with stable version.
-                return false;
-            }
-        } else {
-            throw new IllegalStateException("Invalid release suffix: " + mReleaseSuffix);
-        }
-    }
-}
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index 8c44fe5..41e3241 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -40,9 +40,12 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.StringDef;
+import androidx.car.app.annotations.RequiresCarApi;
 import androidx.car.app.navigation.NavigationManager;
 import androidx.car.app.utils.RemoteUtils;
 import androidx.car.app.utils.ThreadUtils;
+import androidx.car.app.versioning.CarAppApiLevel;
+import androidx.car.app.versioning.CarAppApiLevels;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
 
@@ -54,7 +57,7 @@
  * The CarContext class is a {@link ContextWrapper} subclass accessible to your {@link
  * CarAppService} and {@link Screen} instances, which provides access to car services such as the
  * {@link ScreenManager} for managing the screen stack, the {@link AppManager} for general
- * app-related functionality such as accessing a surface for drawing your navigation app’s map, and
+ * app-related functionality such as accessing a surface for drawing your navigation app's map, and
  * the {@link NavigationManager} used by turn-by-turn navigation apps to communicate navigation
  * metadata and other navigation-related events with the host. See Access the navigation templates
  * for a comprehensive list of library functionality available to navigation apps.
@@ -81,22 +84,22 @@
      *
      * @hide
      */
-    @StringDef({APP_SERVICE, CAR_SERVICE, NAVIGATION_SERVICE, SCREEN_MANAGER_SERVICE})
+    @StringDef({APP_SERVICE, CAR_SERVICE, NAVIGATION_SERVICE, SCREEN_SERVICE})
     @Retention(RetentionPolicy.SOURCE)
     public @interface CarServiceType {
     }
 
     /** Manages all app events such as invalidating the UI, showing a toast, etc. */
-    public static final String APP_SERVICE = "app_manager";
+    public static final String APP_SERVICE = "app";
 
     /**
      * Manages all navigation events such as starting navigation when focus is granted, abandoning
      * navigation when focus is lost, etc.
      */
-    public static final String NAVIGATION_SERVICE = "navigation_manager";
+    public static final String NAVIGATION_SERVICE = "navigation";
 
     /** Manages the screens of the app, including the screen stack. */
-    public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
+    public static final String SCREEN_SERVICE = "screen";
 
     /**
      * Internal usage only. Top level binder to host.
@@ -107,7 +110,8 @@
      * Key for including a IStartCarApp in the notification {@link Intent}, for starting the app
      * if it has not been opened yet.
      */
-    public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+    public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra"
+            + ".START_CAR_APP_BINDER_KEY";
 
     /**
      * Standard action for navigating to a location.
@@ -121,9 +125,12 @@
     private final NavigationManager mNavigationManager;
     private final ScreenManager mScreenManager;
     private final OnBackPressedDispatcher mOnBackPressedDispatcher;
-
     private final HostDispatcher mHostDispatcher;
 
+    /** API level, updated once host connection handshake is completed. */
+    @CarAppApiLevel
+    private int mCarAppApiLevel = CarAppApiLevels.UNKNOWN;
+
     /** @hide */
     @NonNull
     @RestrictTo(LIBRARY)
@@ -143,13 +150,13 @@
      *   <dd>An {@link AppManager} for communication between the app and the host.
      *   <dt>{@link #NAVIGATION_SERVICE}
      *   <dd>A {@link NavigationManager} for management of navigation updates.
-     *   <dt>{@link #SCREEN_MANAGER_SERVICE}
+     *   <dt>{@link #SCREEN_SERVICE}
      *   <dd>A {@link ScreenManager} for management of {@link Screen}s.
      * </dl>
      *
      * @param name The name of the car service requested. This should be one of
      *             {@link #APP_SERVICE},
-     *             {@link #NAVIGATION_SERVICE} or {@link #SCREEN_MANAGER_SERVICE}.
+     *             {@link #NAVIGATION_SERVICE} or {@link #SCREEN_SERVICE}.
      * @return The car service instance.
      * @throws IllegalArgumentException if {@code name} does not refer to a valid car service.
      * @throws NullPointerException     if {@code name} is {@code null}.
@@ -162,7 +169,7 @@
                 return mAppManager;
             case NAVIGATION_SERVICE:
                 return mNavigationManager;
-            case SCREEN_MANAGER_SERVICE:
+            case SCREEN_SERVICE:
                 return mScreenManager;
             default: // fall out
         }
@@ -206,7 +213,7 @@
         } else if (serviceClass.isInstance(mNavigationManager)) {
             return NAVIGATION_SERVICE;
         } else if (serviceClass.isInstance(mScreenManager)) {
-            return SCREEN_MANAGER_SERVICE;
+            return SCREEN_SERVICE;
         }
 
         throw new IllegalArgumentException("The class does not correspond to a car service.");
@@ -215,8 +222,7 @@
     /**
      * Starts a car app on the car screen.
      *
-     * <p>The target application will get the {@link Intent} via
-     * {@link CarAppService#onCreateScreen}
+     * <p>The target application will get the {@link Intent} via {@link Session#onCreateScreen}
      * or {@link CarAppService#onNewIntent}.
      *
      * <p>Supported {@link Intent}s:
@@ -282,7 +288,7 @@
         IBinder binder = null;
         Bundle extras = notificationIntent.getExtras();
         if (extras != null) {
-            binder = extras.getBinder(START_CAR_APP_BINDER_KEY);
+            binder = extras.getBinder(EXTRA_START_CAR_APP_BINDER_KEY);
         }
         if (binder == null) {
             throw new IllegalArgumentException("Notification intent missing expected extra");
@@ -301,10 +307,10 @@
     /**
      * Requests to finish the car app.
      *
-     * <p>Call this when your app is done and should be closed.
+     * <p>Call this when your app is done and should be closed. The {@link Session} corresponding
+     * to this {@link CarContext} will become {@code State.DESTROYED}.
      *
-     * <p>At some point after this call, {@link CarAppService#onCarAppFinished} will be called, and
-     * eventually the OS will destroy your {@link CarAppService}.
+     * <p>At some point after this call, the OS will destroy your {@link CarAppService}.
      */
     public void finishCarApp() {
         mHostDispatcher.dispatch(
@@ -374,6 +380,22 @@
     }
 
     /**
+     * Updates context information based on the information provided during connection handshake
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY)
+    @MainThread
+    void onHandshakeComplete(HandshakeInfo handshakeInfo) {
+        int carAppApiLevel = handshakeInfo.getHostCarAppApiLevel();
+        if (!CarAppApiLevels.isValid(carAppApiLevel)) {
+            throw new IllegalArgumentException("Invalid Car App API level received: "
+                    + carAppApiLevel);
+        }
+        mCarAppApiLevel = carAppApiLevel;
+    }
+
+    /**
      * Attaches the base {@link Context} for this {@link CarContext} by creating a new display
      * context using {@link #createDisplayContext} with a {@link VirtualDisplay} created using
      * the metrics from the provided {@link Configuration}, and then also calling {@link
@@ -431,6 +453,39 @@
         mHostDispatcher.resetHosts();
     }
 
+    /**
+     * Retrieves the API level negotiated with the host.
+     * <p>
+     * API levels are used during client and host connection handshake to negotiate a common set of
+     * elements that both processes can understand. Different devices might have different host
+     * versions. Each of these hosts will support a
+     * range of API levels, as a way to provide backwards compatibility.
+     * <p>
+     * Applications can also provide forward compatibility, by declaring support for a
+     * {@link AppInfo#getMinCarAppApiLevel()} lower than {@link AppInfo#getLatestCarAppApiLevel()}.
+     * See {@link AppInfo#getMinCarAppApiLevel()} for more details.
+     * <p>
+     * Clients must ensure no elements annotated with a {@link RequiresCarApi} value higher
+     * than {@link #getCarAppApiLevel()} is used at runtime.
+     * <p>
+     * Please refer to {@link RequiresCarApi} description for more details on how to
+     * implement forward compatibility.
+     *
+     * @return a value between {@link AppInfo#getMinCarAppApiLevel()} and
+     * {@link AppInfo#getLatestCarAppApiLevel()}. In case of incompatibility, the host will
+     * disconnect from the service before completing the handshake.
+     *
+     * @throws IllegalStateException if invoked before the connection handshake with the host has
+     * been completed (for example, before {@link Session#onCreateScreen(Intent)}).
+     */
+    @CarAppApiLevel
+    public int getCarAppApiLevel() {
+        if (mCarAppApiLevel == CarAppApiLevels.UNKNOWN) {
+            throw new IllegalStateException("Car App API level hasn't been established yet");
+        }
+        return mCarAppApiLevel;
+    }
+
     /** @hide */
     @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
     @SuppressWarnings({
@@ -442,7 +497,7 @@
 
         this.mHostDispatcher = hostDispatcher;
         mAppManager = AppManager.create(this, hostDispatcher);
-        mNavigationManager = NavigationManager.create(hostDispatcher);
+        mNavigationManager = NavigationManager.create(this, hostDispatcher);
         mScreenManager = ScreenManager.create(this, lifecycle);
         mOnBackPressedDispatcher =
                 new OnBackPressedDispatcher(() -> getCarService(ScreenManager.class).pop());
diff --git a/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java b/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java
index 8d3f137..8b92f28 100644
--- a/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java
@@ -33,18 +33,25 @@
 public class HandshakeInfo {
     @Nullable
     private final String mHostPackageName;
+    private final int mHostCarAppApiLevel;
 
-    public HandshakeInfo(@NonNull String hostPackageName) {
-        this.mHostPackageName = hostPackageName;
+    public HandshakeInfo(@NonNull String hostPackageName, int hostCarAppApiLevel) {
+        mHostPackageName = hostPackageName;
+        mHostCarAppApiLevel = hostCarAppApiLevel;
     }
 
     // Used for serialization
     public HandshakeInfo() {
         mHostPackageName = null;
+        mHostCarAppApiLevel = 0;
     }
 
     @NonNull
     public String getHostPackageName() {
         return requireNonNull(mHostPackageName);
     }
+
+    public int getHostCarAppApiLevel() {
+        return mHostCarAppApiLevel;
+    }
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/HostInfo.java b/car/app/app/src/main/java/androidx/car/app/HostInfo.java
index 5b1dad2..7174e185 100644
--- a/car/app/app/src/main/java/androidx/car/app/HostInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/HostInfo.java
@@ -19,9 +19,10 @@
 import static java.util.Objects.requireNonNull;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 
 /**
- * Container class for information about the host the service is connected to.
+ * Container class for information about the host the app is connected to.
  *
  * <p>Apps can use this information to determine how they will respond to the host. For example, a
  * host which is not recognized could receive a message screen while an authorized host could
@@ -29,16 +30,24 @@
  *
  * <p>The package name and uid can used to query the system package manager for a signature or to
  * determine if the host has a system signature.
+ *
+ * <p>The host API level can be used to adjust the models exchanged with the host to those valid
+ * for the specific host version the app is connected to.
  */
 public class HostInfo {
     @NonNull
     private final String mPackageName;
     private final int mUid;
 
-    /** Constructs an instance of the HostInfo from the required package name and uid. */
+    /**
+     * Constructs an instance of the HostInfo from the required package name, uid and API level.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
     public HostInfo(@NonNull String packageName, int uid) {
-        this.mPackageName = requireNonNull(packageName);
-        this.mUid = uid;
+        mPackageName = requireNonNull(packageName);
+        mUid = uid;
     }
 
     /** Retrieves the package name of the host. */
diff --git a/car/app/app/src/main/java/androidx/car/app/host/OnDoneCallback.java b/car/app/app/src/main/java/androidx/car/app/OnDoneCallback.java
similarity index 85%
rename from car/app/app/src/main/java/androidx/car/app/host/OnDoneCallback.java
rename to car/app/app/src/main/java/androidx/car/app/OnDoneCallback.java
index bc6db49..8ace8ef 100644
--- a/car/app/app/src/main/java/androidx/car/app/host/OnDoneCallback.java
+++ b/car/app/app/src/main/java/androidx/car/app/OnDoneCallback.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host;
+package androidx.car.app;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -27,11 +27,15 @@
     /**
      * Notifies that the request has been successfully processed the request and provides a
      * response.
+     *
+     * @param response the {@link Bundleable} containing the success response.
      */
     void onSuccess(@Nullable Bundleable response);
 
     /**
      * Notifies that the request was not fulfilled successfully.
+     *
+     * @param response the {@link Bundleable} containing the failure response.
      */
     void onFailure(@NonNull Bundleable response);
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/OnScreenResultCallback.java b/car/app/app/src/main/java/androidx/car/app/OnScreenResultListener.java
similarity index 92%
rename from car/app/app/src/main/java/androidx/car/app/OnScreenResultCallback.java
rename to car/app/app/src/main/java/androidx/car/app/OnScreenResultListener.java
index 24f0ed3..8e19f38 100644
--- a/car/app/app/src/main/java/androidx/car/app/OnScreenResultCallback.java
+++ b/car/app/app/src/main/java/androidx/car/app/OnScreenResultListener.java
@@ -18,8 +18,8 @@
 
 import androidx.annotation.Nullable;
 
-/** A callback to provide the result set by a {@link Screen}. */
-public interface OnScreenResultCallback {
+/** A listener to provide the result set by a {@link Screen}. */
+public interface OnScreenResultListener {
     /**
      * Provides the {@code result} from the {@link Screen} that was pushed using {@link
      * ScreenManager#pushForResult}, or {@code null} if no result was set.
diff --git a/car/app/app/src/main/java/androidx/car/app/Screen.java b/car/app/app/src/main/java/androidx/car/app/Screen.java
index 62158f0..9fe39a3 100644
--- a/car/app/app/src/main/java/androidx/car/app/Screen.java
+++ b/car/app/app/src/main/java/androidx/car/app/Screen.java
@@ -40,7 +40,7 @@
  * A Screen has a {@link Lifecycle} and provides the mechanism for the app to send {@link Template}s
  * to display when the Screen is visible. Screen instances can also be pushed and popped to and from
  * a Screen stack, which ensures they adhere to the template flow restrictions (see {@link
- * #getTemplate} for more details on template flow).
+ * #onGetTemplate} for more details on template flow).
  *
  * <p>The Screen class can be used to manage individual units of business logic within a car app. A
  * Screen is closely tied to the {@link CarAppService} it is a part of, and cannot be used without
@@ -54,18 +54,12 @@
 // actually cleaning any held resources in that method.
 @SuppressWarnings("NotCloseable")
 public abstract class Screen implements LifecycleOwner {
-    /**
-     * A marker to use with {@link ScreenManager#popTo} when it should pop all the way to the root
-     * screen in the stack.
-     */
-    public static final String ROOT = "ROOT";
-
     private final CarContext mCarContext;
 
     @SuppressWarnings({"assignment.type.incompatible", "argument.type.incompatible"})
     private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
 
-    private OnScreenResultCallback mOnScreenResultCallback = (obj) -> {
+    private OnScreenResultListener mOnScreenResultListener = (obj) -> {
     };
 
     @Nullable
@@ -84,7 +78,7 @@
     /**
      * Whether to set the ID of the last template in the next template to be returned.
      *
-     * @see #getTemplate
+     * @see #onGetTemplate
      */
     private boolean mUseLastTemplateId;
 
@@ -94,15 +88,15 @@
 
     /**
      * Requests the current template to be invalidated, which eventually triggers a call to {@link
-     * #getTemplate} to get the new template to display.
+     * #onGetTemplate} to get the new template to display.
      *
      * <p>If the current {@link State} of this screen is not at least {@link State#STARTED}, then a
      * call to this method will have no effect.
      *
      * <p>After the call to invalidate is made, subsequent calls have no effect until the new
-     * template is returned by {@link #getTemplate}.
+     * template is returned by {@link #onGetTemplate}.
      *
-     * <p>To avoid race conditions with calls to {@link #getTemplate} you should call this method
+     * <p>To avoid race conditions with calls to {@link #onGetTemplate} you should call this method
      * with the main thread.
      *
      * @throws HostException if the remote call fails.
@@ -126,7 +120,7 @@
     }
 
     /**
-     * Sets the {@code result} that will be sent to the {@link OnScreenResultCallback} that was
+     * Sets the {@code result} that will be sent to the {@link OnScreenResultListener} that was
      * given when pushing this screen onto the stack using {@link ScreenManager#pushForResult}.
      *
      * <p>Only the final {@code result} set will be sent.
@@ -134,7 +128,7 @@
      * <p>The {@code result} will be propagated when this screen is being destroyed. This can be due
      * to being removed from the stack or explicitly calling {@link #finish}.
      *
-     * @param result the value to send to the {@link OnScreenResultCallback} that was given when
+     * @param result the value to send to the {@link OnScreenResultListener} that was given when
      *               pushing this screen onto the stack using {@link ScreenManager#pushForResult}
      */
     public void setResult(@Nullable Object result) {
@@ -184,7 +178,7 @@
      *   <dt>{@link Event#ON_CREATE}
      *   <dd>The screen is in the process of being pushed to the screen stack, it is valid, but
      *       contents from it are not yet visible in the car screen. You should get a callback to
-     *       {@link #getTemplate} at a point after this call.
+     *       {@link #onGetTemplate} at a point after this call.
      *   <dt>{@link Event#ON_START}
      *   <dd>The template returned from this screen is visible in the car screen.
      *   <dt>{@link Event#ON_RESUME}
@@ -291,7 +285,7 @@
      * Certain {@link Template} classes have special semantics that signify the end of a task. For
      * example, the {@link androidx.car.app.navigation.model.NavigationTemplate} is a template
      * that is expected to stay on the screen and be refreshed with new turn-by-turn instructions
-     * for the user’s consumption. Upon reaching one of these templates, the host will reset the
+     * for the user's consumption. Upon reaching one of these templates, the host will reset the
      * template quota, treating that template as if it is the first step of a new task, thus
      * allowing the app to begin a new task. See the documentation of individual {@link Template}
      * classes to see which ones trigger a reset on the host.
@@ -304,11 +298,11 @@
      * <p>See {@code androidx.car.app.notification.CarAppExtender} for details on notifications.
      */
     @NonNull
-    public abstract Template getTemplate();
+    public abstract Template onGetTemplate();
 
-    /** Sets a {@link OnScreenResultCallback} for this {@link Screen}. */
-    void setOnResultCallback(OnScreenResultCallback onScreenResultCallback) {
-        this.mOnScreenResultCallback = onScreenResultCallback;
+    /** Sets a {@link OnScreenResultListener} for this {@link Screen}. */
+    void setOnScreenResultListener(OnScreenResultListener onScreenResultListener) {
+        this.mOnScreenResultListener = onScreenResultListener;
     }
 
     /**
@@ -322,7 +316,7 @@
         ThreadUtils.runOnMain(
                 () -> {
                     if (event == Event.ON_DESTROY) {
-                        mOnScreenResultCallback.onScreenResult(mResult);
+                        mOnScreenResultListener.onScreenResult(mResult);
                     }
 
                     mLifecycleRegistry.handleLifecycleEvent(event);
@@ -330,7 +324,7 @@
     }
 
     /**
-     * Calls {@link #getTemplate} to get the next {@link Template} for the screen and returns it
+     * Calls {@link #onGetTemplate} to get the next {@link Template} for the screen and returns it
      * wrapped in a {@link TemplateWrapper}.
      *
      * <p>The {@link TemplateWrapper} attaches a unique ID to the wrapped template, which is used
@@ -345,7 +339,7 @@
      */
     @NonNull
     TemplateWrapper getTemplateWrapper() {
-        Template template = getTemplate();
+        Template template = onGetTemplate();
 
         TemplateWrapper wrapper;
         if (mUseLastTemplateId) {
@@ -368,15 +362,15 @@
      * Returns the information for the template that was last returned by this screen.
      *
      * <p>If no templates have been returned from this screen yet, this will call
-     * {@link #getTemplate} to retrieve the {@link Template} and generate an info for it. This is
-     * used in the case where multiple screens are added before a {@link #getTemplate} method is
+     * {@link #onGetTemplate} to retrieve the {@link Template} and generate an info for it. This is
+     * used in the case where multiple screens are added before a {@link #onGetTemplate} method is
      * dispatched to the top screen, allowing to notify the host of the current stack of template
      * ids known to the client.
      */
     @NonNull
     TemplateInfo getLastTemplateInfo() {
         if (mTemplateWrapper == null) {
-            mTemplateWrapper = TemplateWrapper.wrap(getTemplate());
+            mTemplateWrapper = TemplateWrapper.wrap(onGetTemplate());
         }
         return new TemplateInfo(mTemplateWrapper.getTemplate(), mTemplateWrapper.getId());
     }
@@ -387,8 +381,8 @@
     }
 
     /**
-     * Denotes whether the next {@link Template} retrieved via {@link #getTemplate} should reuse the
-     * ID of the last {@link Template}.
+     * Denotes whether the next {@link Template} retrieved via {@link #onGetTemplate} should reuse
+     * the ID of the last {@link Template}.
      *
      * <p>When this is set to {@code true}, the host will considered the next template sent to be a
      * back operation, and will attempt to find the previous template that shares the same ID and
diff --git a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
index 56b3365..1b9d079 100644
--- a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
@@ -58,7 +58,7 @@
      * @throws NullPointerException if the method is called before a {@link Screen} has been
      *                              pushed to the stack via {@link #push}, or
      *                              {@link #pushForResult}, or returning a {@link Screen} from
-     *                              {@link CarAppService#onCreateScreen}.
+     *                              {@link Session#onCreateScreen}.
      */
     @NonNull
     public Screen getTop() {
@@ -81,7 +81,7 @@
      * Pushes a {@link Screen}, for which you would like a result from, onto the stack.
      *
      * <p>When the given {@code screen} finishes, the {@code onScreenResultCallback} will receive a
-     * callback to {@link OnScreenResultCallback#onScreenResult} with the result that the pushed
+     * callback to {@link OnScreenResultListener#onScreenResult} with the result that the pushed
      * {@code screen} set via {@link Screen#setResult}.
      *
      * @throws NullPointerException if either the {@code screen} or the {@code
@@ -90,8 +90,8 @@
     // TODO(rampara): Add Executor parameter.
     @SuppressLint("ExecutorRegistration")
     public void pushForResult(
-            @NonNull Screen screen, @NonNull OnScreenResultCallback onScreenResultCallback) {
-        requireNonNull(screen).setOnResultCallback(requireNonNull(onScreenResultCallback));
+            @NonNull Screen screen, @NonNull OnScreenResultListener onScreenResultListener) {
+        requireNonNull(screen).setOnScreenResultListener(requireNonNull(onScreenResultListener));
         pushInternal(screen);
     }
 
@@ -110,8 +110,6 @@
      * Removes screens from the top of the stack until a {@link Screen} which has the given {@code
      * marker} is found, or the root has been reached.
      *
-     * <p>To pop to root use {@link Screen#ROOT} as the {@code marker}.
-     *
      * <p>The root {@link Screen} will not be popped.
      *
      * @throws NullPointerException if {@code marker} is {@code null}.
@@ -135,6 +133,23 @@
     }
 
     /**
+     * Removes all screens from the stack until the root has been reached.
+     */
+    public void popToRoot() {
+        if (mScreenStack.size() <= 1) {
+            return;
+        }
+
+        // Pop all screens, except until found root or the provided screen.
+        List<Screen> screensToPop = new ArrayList<>();
+        while (mScreenStack.size() > 1) {
+            screensToPop.add(mScreenStack.pop());
+        }
+
+        popInternal(screensToPop);
+    }
+
+    /**
      * Removes the {@code screen} from the stack.
      *
      * <p>If the {@code screen} is the only {@link Screen} in the stack, it will not be removed.
@@ -202,10 +217,6 @@
     }
 
     private boolean foundMarker(String marker) {
-        if (Screen.ROOT.equals(marker)) {
-            return mScreenStack.size() < 2;
-        }
-
         return marker.equals(getTop().getMarker());
     }
 
diff --git a/car/app/app/src/main/java/androidx/car/app/SearchListener.java b/car/app/app/src/main/java/androidx/car/app/SearchListener.java
deleted file mode 100644
index ed814d4..0000000
--- a/car/app/app/src/main/java/androidx/car/app/SearchListener.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.car.app;
-
-import androidx.annotation.NonNull;
-
-/** A listener for search updates. */
-public interface SearchListener {
-    /**
-     * Notifies the current {@code searchText}.
-     *
-     * <p>The host may invoke this callback as the user types a search text. The frequency of these
-     * updates is not guaranteed to be after every individual keystroke. The host may decide to wait
-     * for several keystrokes before sending a single update.
-     *
-     * @param searchText the current search text that the user has typed.
-     */
-    void onSearchTextChanged(@NonNull String searchText);
-
-    /**
-     * Notifies that the user has submitted the search and the given {@code searchText} is the final
-     * term.
-     *
-     * @param searchText the search text that the user typed.
-     */
-    void onSearchSubmitted(@NonNull String searchText);
-}
diff --git a/car/app/app/src/main/java/androidx/car/app/Session.java b/car/app/app/src/main/java/androidx/car/app/Session.java
new file mode 100644
index 0000000..e77907a
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/Session.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/**
+ * The base class for implementing a session for a car app.
+ */
+public abstract class Session implements LifecycleOwner {
+    private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
+    private final CarContext mCarContext = CarContext.create(mRegistry);
+
+    /**
+     * Requests the first {@link Screen} for the application.
+     *
+     * <p>Once the method returns, {@link Screen#onGetTemplate()} will be called on the
+     * {@link Screen} returned, and the app will be displayed on the car screen.
+     *
+     * <p>To pre-seed a back stack, you can push {@link Screen}s onto the stack, via {@link
+     * ScreenManager#push} during this method call.
+     *
+     * <p>Called by the system, do not call this method directly.
+     *
+     * @param intent the intent that was used to start this app. If the app was started with a
+     *               call to {@link CarContext#startCarApp}, this intent will be equal to the
+     *               intent passed to that method.
+     */
+    @NonNull
+    public abstract Screen onCreateScreen(@NonNull Intent intent);
+
+    /**
+     * Returns the {@link Session}'s {@link Lifecycle}.
+     *
+     * <p>Here are some of the ways you can use the sessions's {@link Lifecycle}:
+     *
+     * <ul>
+     *   <li>Observe its {@link Lifecycle} by calling {@link Lifecycle#addObserver}. You can use the
+     *       {@link androidx.lifecycle.LifecycleObserver} to take specific actions whenever the
+     *       {@link Screen} receives different {@link Lifecycle.Event}s.
+     *   <li>Use this {@link CarAppService} to observe {@link androidx.lifecycle.LiveData}s that
+     *       may drive the backing data for your application.
+     * </ul>
+     *
+     * <p>What each lifecycle related event means for a session:
+     *
+     * <dl>
+     *   <dt>{@link Lifecycle.Event#ON_CREATE}
+     *   <dd>The session has just been launched, and this session is being initialized. {@link
+     *       #onCreateScreen} will be called at a point after this call.
+     *   <dt>{@link #onCreateScreen}
+     *   <dd>The host is ready for this session to create the first {@link Screen} so that it can
+     *       display its template.
+     *   <dt>{@link Lifecycle.Event#ON_START}
+     *   <dd>The application is now visible in the car screen.
+     *   <dt>{@link Lifecycle.Event#ON_RESUME}
+     *   <dd>The user can now interact with this application.
+     *   <dt>{@link Lifecycle.Event#ON_PAUSE}
+     *   <dd>The user can no longer interact with this application.
+     *   <dt>{@link Lifecycle.Event#ON_STOP}
+     *   <dd>The application is no longer visible.
+     *   <dt>{@link Lifecycle.Event#ON_DESTROY}
+     *   <dd>The OS has now destroyed this {@link Session} instance, and it is no longer
+     *       valid.
+     * </dl>
+     *
+     * <p>Listeners that are added in {@link Lifecycle.Event#ON_START}, should be removed in {@link
+     * Lifecycle.Event#ON_STOP}.
+     *
+     * <p>Listeners that are added in {@link Lifecycle.Event#ON_CREATE} should be removed in {@link
+     * Lifecycle.Event#ON_DESTROY}.
+     *
+     * @see androidx.lifecycle.LifecycleObserver
+     */
+    @NonNull
+    @Override
+    public Lifecycle getLifecycle() {
+        return mRegistry;
+    }
+
+    /**
+     * Returns the {@link CarContext} for this session.
+     *
+     * <p><b>The {@link CarContext} is not fully initialized until this session's {@link
+     * Lifecycle.State} is at least {@link Lifecycle.State#CREATED}</b>
+     *
+     * @see #getLifecycle
+     */
+    @NonNull
+    public final CarContext getCarContext() {
+        return mCarContext;
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/annotations/RequiresCarApi.java b/car/app/app/src/main/java/androidx/car/app/annotations/RequiresCarApi.java
new file mode 100644
index 0000000..4201a81
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/annotations/RequiresCarApi.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.annotations;
+
+import androidx.car.app.CarContext;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines the minimum API level required to be able to use this model, field or method.
+ * <p>
+ * Before using any of this elements, application must check that
+ * {@link CarContext#getCarAppApiLevel()} is equal or greater than the value of this annotation.
+ * <p>
+ * For example, if an application wants to use a newer template "Foo" marked with
+ * <code>@RequiresHostApiLevel(2)</code> while maintain backwards compatibility with older hosts
+ * by using an older template "Bar" (<code>@RequiresHostApiLevel(1)</code>), they can do:
+ *
+ * <pre>
+ * if (getCarContext().getCarApiLevel() >= 2) {
+ *     // Use new feature
+ *     return Foo.Builder()....;
+ * } else {
+ *     // Use supported fallback
+ *     return Bar.Builder()....;
+ * }
+ * </pre>
+ *
+ * If a certain model or method has no {@link RequiresCarApi} annotation, it is assumed to
+ * be available in all car API levels.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})
+public @interface RequiresCarApi {
+
+    /**
+     * The minimum API level required to be able to use this model, field or method. Applications
+     * shouldn't use any elements annotated with a {@link RequiresCarApi} greater than
+     * {@link CarContext#getCarAppApiLevel()}
+     */
+    int value();
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Action.java b/car/app/app/src/main/java/androidx/car/app/model/Action.java
index 551e78f..34cb78d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Action.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Action.java
@@ -34,8 +34,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.car.app.CarContext;
-import androidx.car.app.host.model.OnClickListenerWrapper;
-import androidx.car.app.host.model.OnClickListenerWrapperImpl;
 import androidx.car.app.model.constraints.CarIconConstraints;
 import androidx.lifecycle.LifecycleOwner;
 
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
index 19ee2d5..ddd6ef2 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
@@ -117,34 +117,29 @@
         /**
          * Adds an {@link Action} to the list.
          *
+         * @throws IllegalArgumentException if the background color of the action is specified.
          * @throws IllegalArgumentException if {@code action} is a standard action and an action of
          *                                  the same type has already been added.
          * @throws NullPointerException     if {@code action} is {@code null}.
          */
         @NonNull
         public Builder addAction(@NonNull Action action) {
-            int actionType = requireNonNull(action).getType();
+            Action actionObj = requireNonNull(action);
+            int actionType = actionObj.getType();
             if (actionType != Action.TYPE_CUSTOM && mAddedActionTypes.contains(actionType)) {
                 throw new IllegalArgumentException(
                         "Duplicated action types are disallowed: " + action);
             }
+            if (!CarColor.DEFAULT.equals(actionObj.getBackgroundColor())) {
+                throw new IllegalArgumentException(
+                        "Action strip actions don't support background colors");
+            }
             mAddedActionTypes.add(actionType);
             mActions.add(action);
             return this;
         }
 
         /**
-         * Clears any actions that may have been added with {@link #addAction(Action)} up to this
-         * point.
-         */
-        @NonNull
-        public Builder clearActions() {
-            mActions.clear();
-            mAddedActionTypes.clear();
-            return this;
-        }
-
-        /**
          * Constructs the {@link ActionStrip} defined by this builder.
          *
          * @throws IllegalStateException if the action strip is empty.
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
index ae04556..5cf23ac 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
@@ -49,7 +49,8 @@
  *
  * <p>Similar to Android devices, car screens cover a wide range of pixel densities. To ensure that
  * icons and images render well across all car screens, use vector assets whenever possible to avoid
- * scaling issues.
+ * scaling issues. If you use a bitmap instead, ensure that you have resources that address multiple
+ * pixel density buckets.
  *
  * <p>In order to support all car screen sizes and pixel density, you can use configuration
  * qualifiers in your resource files (e.g. "mdpi", "hdpi", etc). See
@@ -111,7 +112,6 @@
                     TYPE_ALERT,
                     TYPE_APP,
                     TYPE_ERROR,
-                    TYPE_WILLIAM_ALERT,
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CarIconType {
@@ -156,13 +156,6 @@
     public static final int TYPE_ERROR = 6;
 
     /**
-     * An alerting William.
-     *
-     * @see #WILLIAM_ALERT
-     */
-    public static final int TYPE_WILLIAM_ALERT = 7;
-
-    /**
      * Represents the app's icon, as defined in the app's manifest by the {@code android:icon}
      * attribute of the {@code application} element.
      */
@@ -178,10 +171,6 @@
     @NonNull
     public static final CarIcon ERROR = CarIcon.forStandardType(TYPE_ERROR);
 
-    @NonNull
-    public static final CarIcon WILLIAM_ALERT =
-            CarIcon.forStandardType(TYPE_WILLIAM_ALERT, /* tint= */ null);
-
     @Keep
     @CarIconType
     private final int mType;
@@ -344,8 +333,6 @@
                 return "APP";
             case TYPE_ERROR:
                 return "ERROR";
-            case TYPE_WILLIAM_ALERT:
-                return "WILLIAM_ALERT";
             case TYPE_BACK:
                 return "BACK";
             case TYPE_CUSTOM:
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java b/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java
index 9fe03a8..3ae6a5f 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/DistanceSpan.java
@@ -38,7 +38,7 @@
  * the string before the interpunct:
  *
  * <pre>{@code
- * String interpunct = "\u00b7";
+ * String interpunct = "\\u00b7";
  * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
  * string.setSpan(
  *   DistanceSpan.create(
@@ -52,7 +52,7 @@
  * apply styling to the text, such as changing colors:
  *
  * <pre>{@code
- * String interpunct = "\u00b7";
+ * String interpunct = "\\u00b7";
  * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
  * string.setSpan(
  *   DistanceSpan.create(
diff --git a/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java b/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java
index 05e58a9..ac05662 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/DurationSpan.java
@@ -36,7 +36,7 @@
  * the string before the interpunct:
  *
  * <pre>{@code
- * String interpunct = "\u00b7";
+ * String interpunct = "\\u00b7";
  * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
  * string.setSpan(DurationSpan.create(300), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
  * }</pre>
@@ -48,7 +48,7 @@
  * apply styling to the text, such as changing colors:
  *
  * <pre>{@code
- * String interpunct = "\u00b7";
+ * String interpunct = "\\u00b7";
  * SpannableString string = new SpannableString("  " + interpunct + " Point-of-Interest 1");
  * string.setSpan(DurationSpan.create(300), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
  * string.setSpan(ForegroundCarColorSpan.create(CarColor.BLUE), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE);
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index 49e6d3a..7737be0 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -28,8 +28,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.car.app.host.model.OnClickListenerWrapper;
-import androidx.car.app.host.model.OnClickListenerWrapperImpl;
 import androidx.car.app.model.constraints.CarIconConstraints;
 
 import java.lang.annotation.Retention;
@@ -69,7 +67,7 @@
     /**
      * Represents a large image to be displayed in the grid item.
      *
-     * <p>If necessary, these images will be scaled down to fit within a 80 x 80 dp bounding box,
+     * <p>If necessary, these images will be scaled down to fit within a 64 x 64 dp bounding box,
      * preserving their aspect ratio.
      */
     public static final int IMAGE_TYPE_LARGE = (1 << 1);
@@ -100,9 +98,9 @@
     }
 
     /** Returns the title of the grid item. */
-    @Nullable
+    @NonNull
     public CarText getTitle() {
-        return mTitle;
+        return requireNonNull(mTitle);
     }
 
     /** Returns the list of text below the title. */
@@ -210,10 +208,19 @@
         @Nullable
         private OnClickListenerWrapper mOnClickListener;
 
-        /** Sets the title of the grid item, or {@code null} to not show the title. */
+        /**
+         * Sets the title of the row.
+         *
+         * @throws NullPointerException     if {@code title} is {@code null}.
+         * @throws IllegalArgumentException if {@code title} is empty.
+         */
         @NonNull
-        public Builder setTitle(@Nullable CharSequence title) {
-            this.mTitle = title == null ? null : CarText.create(title);
+        public Builder setTitle(@NonNull CharSequence title) {
+            CarText titleText = CarText.create(requireNonNull(title));
+            if (titleText.isEmpty()) {
+                throw new IllegalArgumentException("The title cannot be null or empty");
+            }
+            this.mTitle = titleText;
             return this;
         }
 
@@ -223,9 +230,7 @@
          *
          * <h2>Text Wrapping</h2>
          *
-         * The string added with {@link #setText} is truncated at the end to fit in a single line
-         * below
-         * the title.
+         * This text is truncated at the end to fit in a single line below the title.
          */
         @NonNull
         public Builder setText(@Nullable CharSequence text) {
@@ -316,9 +321,8 @@
                 throw new IllegalStateException("An image must be set on the grid item");
             }
 
-            if (mTitle == null && mText != null) {
-                throw new IllegalStateException(
-                        "If a grid item doesn't have a title, it must not have a text set");
+            if (mTitle == null) {
+                throw new IllegalStateException("A title must be set on the grid item");
             }
 
             if (mToggle != null && mOnClickListener != null) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java
index a3a4ddf..199d19d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridTemplate.java
@@ -25,7 +25,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.model.constraints.CarIconConstraints;
-import androidx.car.app.utils.Logger;
 
 import java.util.Collections;
 import java.util.Objects;
@@ -36,7 +35,7 @@
  * <h4>Template Restrictions</h4>
  *
  * In regards to template refreshes, as described in
- * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * {@link androidx.car.app.Screen#onGetTemplate()}, this template is considered a refresh of a
  * previous one if:
  *
  * <ul>
@@ -102,35 +101,6 @@
         return mBackgroundImage;
     }
 
-    @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        requireNonNull(oldTemplate);
-
-        if (oldTemplate.getClass() != this.getClass()) {
-            return false;
-        }
-
-        GridTemplate old = (GridTemplate) oldTemplate;
-
-        if (!Objects.equals(old.getTitle(), getTitle())) {
-            return false;
-        }
-
-        if (old.mIsLoading) {
-            // Transition from a previous loading state is allowed.
-            return true;
-        } else if (mIsLoading) {
-            // Transition to a loading state is disallowed.
-            return false;
-        }
-
-        if (mSingleList != null && old.mSingleList != null) {
-            return mSingleList.isRefresh(old.mSingleList, logger);
-        }
-
-        return true;
-    }
-
     @NonNull
     @Override
     public String toString() {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
index 2a1dcd0..10a898d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
@@ -26,16 +26,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.IOnDoneCallback;
-import androidx.car.app.IOnItemVisibilityChangedListener;
-import androidx.car.app.IOnSelectedListener;
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.host.OnItemVisibilityChangedListenerWrapper;
-import androidx.car.app.host.OnSelectedListenerWrapper;
-import androidx.car.app.host.model.OnClickListenerWrapper;
-import androidx.car.app.utils.Logger;
 import androidx.car.app.utils.RemoteUtils;
-import androidx.car.app.utils.ValidationUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -51,7 +44,7 @@
     /**
      * A listener for handling selection events for lists with selectable items.
      *
-     * @see Builder#setSelectable(OnSelectedListener)
+     * @see Builder#setOnSelectedListener(OnSelectedListener)
      */
     public interface OnSelectedListener {
         /**
@@ -126,7 +119,7 @@
      * items in the list changes.
      */
     @Nullable
-    public OnItemVisibilityChangedListenerWrapper getOnItemsVisibilityChangeListener() {
+    public OnItemVisibilityChangedListenerWrapper getOnItemsVisibilityChangedListener() {
         return mOnItemVisibilityChangedListener;
     }
 
@@ -136,30 +129,6 @@
         return mItems;
     }
 
-    /**
-     * Returns {@code true} if this {@link ItemList} instance is determined to be a refresh of the
-     * given list, or {@code false} otherwise.
-     *
-     * <p>A list is considered a refresh if:
-     *
-     * <ul>
-     *   <li>The other list is in a loading state, or
-     *   <li>The item size and string contents of the two lists are the same. For rows that
-     *   contain a
-     *       {@link Toggle}, the string contents can be updated if the toggle state has changed
-     *       between the previous and new rows. For grid items that contain a {@link Toggle}, string
-     *       contents and images can be updated if the toggle state has changed.
-     * </ul>
-     */
-    public boolean isRefresh(@Nullable ItemList other, @NonNull Logger logger) {
-        if (other == null) {
-            return false;
-        }
-
-        return ValidationUtils.itemsHaveSameContent(
-                other.getItems(), other.getSelectedIndex(), getItems(), getSelectedIndex(), logger);
-    }
-
     @Override
     @NonNull
     public String toString() {
@@ -238,7 +207,7 @@
          */
         @NonNull
         @SuppressLint("ExecutorRegistration")
-        public Builder setOnItemsVisibilityChangeListener(
+        public Builder setOnItemsVisibilityChangedListener(
                 @Nullable OnItemVisibilityChangedListener itemVisibilityChangedListener) {
             this.mOnItemVisibilityChangedListener =
                     itemVisibilityChangedListener == null
@@ -248,28 +217,24 @@
         }
 
         /**
-         * Marks the list as selectable and sets the {@link OnSelectedListener} to call when an
-         * item is selected by the user. Set to {@code null} to mark the list as non-selectable.
+         * Marks the list as selectable by setting the {@link OnSelectedListener} to call when an
+         * item is selected by the user, or set to {@code null} to mark the list as non-selectable.
          *
          * <p>Selectable lists, where allowed by the template they are added to, automatically
-         * display
-         * an item in a selected state when selected by the user.
+         * display an item in a selected state when selected by the user.
          *
          * <p>The items in the list define a mutually exclusive selection scope: only a single
-         * item will
-         * be selected at any given time.
+         * item will be selected at any given time.
          *
          * <p>The specific way in which the selection will be visualized depends on the template
-         * and the
-         * host implementation. For example, some templates may display the list as a radio button
-         * group, while others may highlight the selected item's background.
+         * and the host implementation. For example, some templates may display the list as a
+         * radio button group, while others may highlight the selected item's background.
          *
          * @see #setSelectedIndex(int)
          */
         @NonNull
-        // TODO(rampara): Review if API should be updated to match getter.
-        @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
-        public Builder setSelectable(@Nullable OnSelectedListener onSelectedListener) {
+        @SuppressLint("ExecutorRegistration")
+        public Builder setOnSelectedListener(@Nullable OnSelectedListener onSelectedListener) {
             this.mOnSelectedListener =
                     onSelectedListener == null ? null : createOnSelectedListener(
                             onSelectedListener);
@@ -281,8 +246,8 @@
          *
          * <p>By default and unless explicitly set with this method, the first item is selected.
          *
-         * <p>If the list is not a selectable list set with {@link #setSelectable}, this value is
-         * ignored.
+         * <p>If the list is not a selectable list set with {@link #setOnSelectedListener}, this
+         * value is ignored.
          */
         @NonNull
         public Builder setSelectedIndex(int selectedIndex) {
@@ -298,8 +263,7 @@
          * Sets the text to display if the list is empty.
          *
          * <p>If the list is empty and the app does not explicitly set the message with this
-         * method, the
-         * host will show a default message.
+         * method, the host will show a default message.
          */
         @NonNull
         public Builder setNoItemsMessage(@Nullable CharSequence noItemsMessage) {
@@ -330,8 +294,7 @@
          *
          * @throws IllegalStateException if the list is selectable but does not have any items.
          * @throws IllegalStateException if the selected index is greater or equal to the size of
-         *                               the
-         *                               list.
+         *                               the list.
          * @throws IllegalStateException if the list is selectable and any items have either one of
          *                               their {@link OnClickListener} or {@link Toggle} set.
          */
@@ -355,8 +318,8 @@
                     if (getOnClickListener(item) != null) {
                         throw new IllegalStateException(
                                 "Items that belong to selectable lists can't have an "
-                                        + "onClickListener. Use the"
-                                        + " OnSelectedListener of the list instead");
+                                        + "onClickListener. Use the OnSelectedListener of the list "
+                                        + "instead");
                     }
 
                     if (getToggle(item) != null) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java
index d3da538..dda936c 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/ListTemplate.java
@@ -30,7 +30,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.car.app.Screen;
-import androidx.car.app.utils.Logger;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -42,7 +41,7 @@
  *
  * <h4>Template Restrictions</h4>
  *
- * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this
+ * In regards to template refreshes, as described in {@link Screen#onGetTemplate()}, this
  * template is considered a refresh of a previous one if:
  *
  * <ul>
@@ -109,49 +108,6 @@
         return mActionStrip;
     }
 
-    @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        requireNonNull(oldTemplate);
-
-        if (oldTemplate.getClass() != this.getClass()) {
-            return false;
-        }
-
-        ListTemplate old = (ListTemplate) oldTemplate;
-
-        if (!Objects.equals(old.getTitle(), getTitle())) {
-            return false;
-        }
-
-        if (old.mIsLoading) {
-            // Transition from a previous loading state is allowed.
-            return true;
-        } else if (mIsLoading) {
-            // Transition to a loading state is disallowed.
-            return false;
-        }
-
-        if (mSingleList != null && old.mSingleList != null) {
-            return mSingleList.isRefresh(old.mSingleList, logger);
-        } else {
-            if (mSectionLists.size() != old.mSectionLists.size()) {
-                return false;
-            }
-
-            for (int i = 0; i < mSectionLists.size(); i++) {
-                SectionedItemList section = mSectionLists.get(i);
-                SectionedItemList oldSection = old.mSectionLists.get(i);
-
-                if (!section.getHeader().equals(oldSection.getHeader())
-                        || !section.getItemList().isRefresh(oldSection.getItemList(), logger)) {
-                    return false;
-                }
-            }
-        }
-
-        return true;
-    }
-
     @NonNull
     @Override
     public String toString() {
@@ -266,15 +222,6 @@
             return this;
         }
 
-        /** Resets any list(s) that were added via {@link #setSingleList} or {@link #addList}. */
-        @NonNull
-        public Builder clearAllLists() {
-            mSingleList = null;
-            mSectionLists.clear();
-            mHasSelectableList = false;
-            return this;
-        }
-
         /**
          * Sets a single {@link ItemList} to show in the template.
          *
@@ -335,7 +282,7 @@
                 throw new IllegalArgumentException("List cannot be empty");
             }
 
-            if (list.getOnItemsVisibilityChangeListener() != null) {
+            if (list.getOnItemsVisibilityChangedListener() != null) {
                 throw new IllegalArgumentException(
                         "OnItemVisibilityChangedListener in the list is disallowed");
             }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
index 90beda5..baa90dc 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
@@ -27,7 +27,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.model.constraints.CarIconConstraints;
-import androidx.car.app.utils.Logger;
 
 import java.util.Collections;
 import java.util.List;
@@ -39,7 +38,7 @@
  * <h4>Template Restrictions</h4>
  *
  * In regards to template refreshes, as described in
- * {@link androidx.car.app.Screen#getTemplate()}, this template is
+ * {@link androidx.car.app.Screen#onGetTemplate()}, this template is
  * considered a refresh of a previous one if the title and messages have not changed.
  */
 public final class MessageTemplate implements Template {
@@ -94,24 +93,10 @@
     }
 
     @Nullable
-    public ActionList getActionList() {
+    public ActionList getActions() {
         return mActionList;
     }
 
-    @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        requireNonNull(oldTemplate);
-
-        if (oldTemplate.getClass() != this.getClass()) {
-            return false;
-        }
-
-        MessageTemplate old = (MessageTemplate) oldTemplate;
-        return Objects.equals(old.getTitle(), getTitle())
-                && Objects.equals(old.getDebugMessage(), getDebugMessage())
-                && Objects.equals(old.getMessage(), getMessage());
-    }
-
     @NonNull
     @Override
     public String toString() {
@@ -288,8 +273,6 @@
          * @throws NullPointerException if {@code actions} is {@code null}.
          */
         @NonNull
-        // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
-        @SuppressLint("MissingGetterMatchingBuilder")
         public Builder setActions(@NonNull List<Action> actions) {
             mActionList = ActionList.create(requireNonNull(actions));
             return this;
diff --git a/car/app/app/src/main/java/androidx/car/app/host/OnCheckedChangeListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeListenerWrapper.java
similarity index 93%
rename from car/app/app/src/main/java/androidx/car/app/host/OnCheckedChangeListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeListenerWrapper.java
index c6f7ba1..603323c 100644
--- a/car/app/app/src/main/java/androidx/car/app/host/OnCheckedChangeListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeListenerWrapper.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host;
+package androidx.car.app.model;
 
 import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
 
 /**
  * A host-side interface for reporting to clients that the checked state has changed.
diff --git a/car/app/app/src/main/java/androidx/car/app/host/model/OnClickListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java
similarity index 92%
rename from car/app/app/src/main/java/androidx/car/app/host/model/OnClickListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java
index 64f2691c..fd9d619 100644
--- a/car/app/app/src/main/java/androidx/car/app/host/model/OnClickListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapper.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host.model;
+package androidx.car.app.model;
 
 import androidx.annotation.NonNull;
-import androidx.car.app.host.OnDoneCallback;
+import androidx.car.app.OnDoneCallback;
 
 /**
  * A host-side interface for reporting click to clients.
diff --git a/car/app/app/src/main/java/androidx/car/app/host/model/OnClickListenerWrapperImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapperImpl.java
similarity index 92%
rename from car/app/app/src/main/java/androidx/car/app/host/model/OnClickListenerWrapperImpl.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapperImpl.java
index fa695a8..be60b6f 100644
--- a/car/app/app/src/main/java/androidx/car/app/host/model/OnClickListenerWrapperImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickListenerWrapperImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host.model;
+package androidx.car.app.model;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 
@@ -26,11 +26,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.model.IOnClickListener;
-import androidx.car.app.model.OnClickListener;
-import androidx.car.app.model.ParkedOnlyOnClickListener;
 import androidx.car.app.utils.RemoteUtils;
 
 /**
diff --git a/car/app/app/src/main/java/androidx/car/app/host/OnItemVisibilityChangedListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedListenerWrapper.java
similarity index 95%
rename from car/app/app/src/main/java/androidx/car/app/host/OnItemVisibilityChangedListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedListenerWrapper.java
index e536430..62e3e5b 100644
--- a/car/app/app/src/main/java/androidx/car/app/host/OnItemVisibilityChangedListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedListenerWrapper.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host;
+package androidx.car.app.model;
 
 import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
 
 /**
  * A host-side interface for reporting to clients that the visibility state has changed.
diff --git a/car/app/app/src/main/java/androidx/car/app/host/OnSelectedListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
similarity index 79%
rename from car/app/app/src/main/java/androidx/car/app/host/OnSelectedListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
index 3bef05c..92e3006 100644
--- a/car/app/app/src/main/java/androidx/car/app/host/OnSelectedListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host;
+package androidx.car.app.model;
 
 import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
 
 /**
  * A host-side interface for reporting to clients that an item was selected.
@@ -27,6 +28,10 @@
      *
      * <p>This event is called even if the selection did not change, for example, if the user
      * selected an already selected item.
+     *
+     * @param selectedIndex the index of the selected item.
+     * @param callback      the {@link OnDoneCallback} to trigger when the client finishes handling
+     *                      the event.
      */
     void onSelected(int selectedIndex, @NonNull OnDoneCallback callback);
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Pane.java b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
index 0670466..08e486b 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Pane.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
@@ -18,13 +18,9 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.SuppressLint;
-
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.car.app.utils.Logger;
-import androidx.car.app.utils.ValidationUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -54,7 +50,7 @@
      * Returns the list of {@link Action}s displayed alongside the {@link Row}s in this pane.
      */
     @Nullable
-    public ActionList getActionList() {
+    public ActionList getActions() {
         return mActionList;
     }
 
@@ -73,29 +69,6 @@
         return mIsLoading;
     }
 
-    /**
-     * Returns {@code true} if this {@link Pane} instance is determined to be a refresh of the given
-     * pane, or {@code false} otherwise.
-     *
-     * <p>A pane is considered a refresh if:
-     *
-     * <ul>
-     *   <li>The other pane is in a loading state, or
-     *   <li>The row size and string contents of the two panes are the same.
-     * </ul>
-     */
-    public boolean isRefresh(@Nullable Pane other, @NonNull Logger logger) {
-        if (other == null) {
-            return false;
-        } else if (other.isLoading()) {
-            return true;
-        } else if (isLoading()) {
-            return false;
-        }
-
-        return ValidationUtils.itemsHaveSameContent(other.getRows(), getRows(), logger);
-    }
-
     @Override
     @NonNull
     public String toString() {
@@ -174,13 +147,6 @@
             return this;
         }
 
-        /** Clears any rows that may have been added with {@link #addRow(Row)} up to this point. */
-        @NonNull
-        public Builder clearRows() {
-            mRows.clear();
-            return this;
-        }
-
         /**
          * Sets multiple {@link Action}s to display alongside the rows in the pane.
          *
@@ -189,8 +155,6 @@
          * @throws NullPointerException if {@code actions} is {@code null}.
          */
         @NonNull
-        // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
-        @SuppressLint("MissingGetterMatchingBuilder")
         public Builder setActions(@NonNull List<Action> actions) {
             mActionList = ActionList.create(requireNonNull(actions));
             return this;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
index 37597b6..c2e8a4b 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/PaneTemplate.java
@@ -27,7 +27,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.car.app.utils.Logger;
 
 import java.util.Collections;
 import java.util.Objects;
@@ -38,7 +37,7 @@
  * <h4>Template Restrictions</h4>
  *
  * In regards to template refreshes, as described in
- * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * {@link androidx.car.app.Screen#onGetTemplate()}, this template is considered a refresh of a
  * previous one if:
  *
  * <ul>
@@ -92,17 +91,6 @@
         return mActionStrip;
     }
 
-    @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        if (oldTemplate.getClass() != this.getClass()) {
-            return false;
-        }
-
-        PaneTemplate old = (PaneTemplate) oldTemplate;
-        return Objects.equals(old.getTitle(), getTitle()) && getPane().isRefresh(old.getPane(),
-                logger);
-    }
-
     @NonNull
     @Override
     public String toString() {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
index 1cec76f..187c3c8 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
@@ -21,8 +21,6 @@
 import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
 import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
 
-import static java.util.Objects.requireNonNull;
-
 import android.Manifest.permission;
 import android.content.Context;
 
@@ -31,7 +29,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.car.app.CarAppPermission;
-import androidx.car.app.utils.Logger;
 
 import java.util.Collections;
 import java.util.List;
@@ -46,7 +43,7 @@
  * <h4>Template Restrictions</h4>
  *
  * In regards to template refreshes, as described in
- * {@link androidx.car.app.Screen#getTemplate()}, this template is considered a refresh of a
+ * {@link androidx.car.app.Screen#onGetTemplate()}, this template is considered a refresh of a
  * previous one if:
  *
  * <ul>
@@ -117,29 +114,6 @@
     }
 
     @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        requireNonNull(oldTemplate);
-        if (oldTemplate.getClass() != this.getClass()) {
-            return false;
-        }
-
-        PlaceListMapTemplate old = (PlaceListMapTemplate) oldTemplate;
-        if (!Objects.equals(old.getTitle(), getTitle())) {
-            return false;
-        }
-
-        if (old.mIsLoading) {
-            // Transition from a previous loading state is allowed.
-            return true;
-        } else if (mIsLoading) {
-            // Transition to a loading state is disallowed.
-            return false;
-        }
-
-        return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
-    }
-
-    @Override
     public void checkPermissions(@NonNull Context context) {
         if (isCurrentLocationEnabled()) {
             CarAppPermission.checkHasPermission(context, permission.ACCESS_FINE_LOCATION);
@@ -246,8 +220,7 @@
 
         /**
          * Sets the {@link Action} that will be displayed in the header of the template, or
-         * {@code null}
-         * to not display an action.
+         * {@code null} to not display an action.
          *
          * <h4>Requirements</h4>
          *
@@ -268,8 +241,7 @@
 
         /**
          * Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
-         * display
-         * a title.
+         * display a title.
          */
         @NonNull
         public Builder setTitle(@Nullable CharSequence title) {
@@ -279,32 +251,27 @@
 
         /**
          * Sets an {@link ItemList} to show in a list view along with the map, or {@code null} to
-         * not
-         * display a list.
+         * not display a list.
          *
          * <p>To show a marker corresponding to a point of interest represented by a row, set the
-         * {@link
-         * Place} instance via {@link Row.Builder#setMetadata}. The host will display the {@link
-         * PlaceMarker} in both the map and the list view as the row becomes visible.
+         * {@link Place} instance via {@link Row.Builder#setMetadata}. The host will display the
+         * {@link PlaceMarker} in both the map and the list view as the row becomes visible.
          *
          * <h4>Requirements</h4>
          *
          * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
-         * ignore any
-         * items over that limit. The list itself cannot be selectable as set via {@link
-         * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
-         * {@link
-         * Row.Builder#addText} and cannot contain a {@link Toggle}.
+         * ignore any items over that limit. The list itself cannot be selectable as set via {@link
+         * ItemList.Builder#setOnSelectedListener}. Each {@link Row} can add up to 2 lines of texts
+         * via {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
          *
          * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
          *
          * <p>Rows are not allowed to have both and an image and a place marker.
          *
          * <p>All non-browsable rows must have a {@link DistanceSpan} attached to either its
-         * title or
-         * texts to indicate the distance of the point of interest from the current location. A
-         * row is
-         * browsable when it's configured like so with {@link Row.Builder#setBrowsable(boolean)}.
+         * title or texts to indicate the distance of the point of interest from the current
+         * location. A row is browsable when it's configured like so with
+         * {@link Row.Builder#setBrowsable(boolean)}.
          *
          * @throws IllegalArgumentException if {@code itemList} does not meet the template's
          *                                  requirements.
@@ -339,8 +306,7 @@
          *
          * This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
          * {@link Action}s, one of them can contain a title as set via
-         * {@link Action.Builder#setTitle}.
-         * Otherwise, only {@link Action}s with icons are allowed.
+         * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
          *
          * @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
          *                                  requirements.
@@ -359,13 +325,11 @@
          * <p>The anchor marker is displayed differently from other markers by the host.
          *
          * <p>If not {@code null}, an anchor marker will be shown at the specified {@link LatLng}
-         * on the
-         * map. The camera will adapt to always have the anchor marker visible within its viewport,
-         * along with other places' markers from {@link Row} that are currently visible in the
-         * {@link
-         * Pane}. This can be used to provide a reference point on the map (e.g. the center of a
-         * search
-         * region) as the user pages through the {@link Pane}'s markers, for example.
+         * on the map. The camera will adapt to always have the anchor marker visible within its
+         * viewport, along with other places' markers from {@link Row} that are currently visible
+         * in the {@link Pane}. This can be used to provide a reference point on the map (e.g.
+         * the center of a search region) as the user pages through the {@link Pane}'s markers,
+         * for example.
          */
         @NonNull
         public Builder setAnchor(@Nullable Place anchor) {
@@ -381,8 +345,7 @@
          * Either a header {@link Action} or title must be set on the template.
          *
          * @throws IllegalArgumentException if the template is in a loading state but the list is
-         *                                  set,
-         *                                  or vice versa.
+         *                                  set, or vice versa.
          * @throws IllegalStateException    if the template does not have either a title or header
          *                                  {@link Action} set.
          */
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 8e0f6a9..34a0e23 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -29,8 +29,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.car.app.host.model.OnClickListenerWrapper;
-import androidx.car.app.host.model.OnClickListenerWrapperImpl;
 import androidx.car.app.model.constraints.CarIconConstraints;
 
 import java.lang.annotation.Retention;
@@ -45,20 +43,6 @@
  */
 public class Row implements Item {
     /**
-     * Represents flags that control some attributes of the row.
-     *
-     * @hide
-     */
-    // TODO(shiufai): investigate how to expose IntDefs if needed.
-    @RestrictTo(LIBRARY)
-    @IntDef(
-            value = {ROW_FLAG_NONE, ROW_FLAG_SHOW_DIVIDERS, ROW_FLAG_SECTION_HEADER},
-            flag = true)
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface RowFlags {
-    }
-
-    /**
      * The type of images supported within rows.
      *
      * @hide
@@ -71,26 +55,6 @@
     }
 
     /**
-     * No flags applied to the row.
-     */
-    public static final int ROW_FLAG_NONE = (1 << 0);
-
-    /**
-     * Whether to show dividers around the row.
-     */
-    public static final int ROW_FLAG_SHOW_DIVIDERS = (1 << 1);
-
-    /**
-     * Whether the row is a section header.
-     *
-     * <p>Sections are used to group rows in the UI, for example, by showing them all within a block
-     * of the same background color.
-     *
-     * <p>A section header is a string of text above the section with a title for it.
-     */
-    public static final int ROW_FLAG_SECTION_HEADER = (1 << 2);
-
-    /**
      * Represents a small image to be displayed in the row.
      *
      * <p>If necessary, small images will be scaled down to fit within a 36 x 36 dp bounding box,
@@ -136,9 +100,6 @@
     @Keep
     private final Metadata mMetadata;
     @Keep
-    @RowFlags
-    private final int mFlags;
-    @Keep
     private final boolean mIsBrowsable;
     @Keep
     @RowImageType
@@ -211,14 +172,6 @@
     }
 
     /**
-     * Returns the flags for the row.
-     */
-    @RowFlags
-    public int getFlags() {
-        return mFlags;
-    }
-
-    /**
      * Rows your boat.
      *
      * <p>Example usage:
@@ -259,7 +212,6 @@
                 mToggle,
                 mOnClickListener == null,
                 mMetadata,
-                mFlags,
                 mIsBrowsable,
                 mRowImageType);
     }
@@ -281,7 +233,6 @@
                 && Objects.equals(mToggle, otherRow.mToggle)
                 && Objects.equals(mOnClickListener == null, otherRow.mOnClickListener == null)
                 && Objects.equals(mMetadata, otherRow.mMetadata)
-                && mFlags == otherRow.mFlags
                 && mIsBrowsable == otherRow.mIsBrowsable
                 && mRowImageType == otherRow.mRowImageType;
     }
@@ -294,7 +245,6 @@
         mOnClickListener = builder.mOnClickListener;
         mMetadata = builder.mMetadata;
         mIsBrowsable = builder.mIsBrowsable;
-        mFlags = builder.mFlags;
         mRowImageType = builder.mRowImageType;
     }
 
@@ -307,7 +257,6 @@
         mOnClickListener = null;
         mMetadata = EMPTY_METADATA;
         mIsBrowsable = false;
-        mFlags = ROW_FLAG_NONE;
         mRowImageType = IMAGE_TYPE_SMALL;
     }
 
@@ -324,8 +273,6 @@
         private OnClickListenerWrapper mOnClickListener;
         private Metadata mMetadata = EMPTY_METADATA;
         private boolean mIsBrowsable;
-        @RowFlags
-        private int mFlags = ROW_FLAG_NONE;
         @RowImageType
         private int mRowImageType = IMAGE_TYPE_SMALL;
 
@@ -346,18 +293,6 @@
         }
 
         /**
-         * Sets the title of the row, or {@code null} to not show a title.
-         *
-         * @hide
-         */
-        @RestrictTo(LIBRARY)
-        @NonNull
-        public Builder setTitle(@Nullable CarText title) {
-            this.mTitle = title;
-            return this;
-        }
-
-        /**
          * Adds a text string to the row below the title.
          *
          * <p>The text's color can be customized with {@link ForegroundCarColorSpan} instances.
@@ -367,7 +302,7 @@
          *
          * <h4>Text Wrapping</h4>
          *
-         * Each string added with {@link #addText} will not wrap more than 1 line in the UI, with
+         * Each string added with this method will not wrap more than 1 line in the UI, with
          * one exception: if the template allows a maximum number of text strings larger than 1, and
          * the app adds a single text string, then this string will wrap up to the maximum.
          *
@@ -428,29 +363,6 @@
         }
 
         /**
-         * Clears any rows that may have been added with {@link #addText(CharSequence)} up to this
-         * point.
-         */
-        @NonNull
-        public Builder clearText() {
-            mTexts.clear();
-            return this;
-        }
-
-        /**
-         * Adds a line text of the row below the title.
-         *
-         * @throws NullPointerException if {@code text} is {@code null}.
-         * @hide
-         */
-        @RestrictTo(LIBRARY)
-        @NonNull
-        public Builder addText(@NonNull CarText text) {
-            this.mTexts.add(requireNonNull(text));
-            return this;
-        }
-
-        /**
          * Sets an image to show in the row with the default size {@link #IMAGE_TYPE_SMALL}, or
          * {@code null} to not display an image in the row.
          *
@@ -546,15 +458,6 @@
         }
 
         /**
-         * Sets flags for the row.
-         */
-        @NonNull
-        public Builder setFlags(@RowFlags int flags) {
-            this.mFlags = flags;
-            return this;
-        }
-
-        /**
          * Constructs the {@link Row} defined by this builder.
          *
          * @throws IllegalStateException if the row's title is not set.
diff --git a/car/app/app/src/main/java/androidx/car/app/host/SearchListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/SearchListenerWrapper.java
similarity index 95%
rename from car/app/app/src/main/java/androidx/car/app/host/SearchListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/SearchListenerWrapper.java
index 75036fa..d331059 100644
--- a/car/app/app/src/main/java/androidx/car/app/host/SearchListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchListenerWrapper.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host;
+package androidx.car.app.model;
 
 import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
 
 /**
  * A host-side interface for reporting to search updates to clients.
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
index 5cca4cd..e9c7724 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
@@ -28,13 +28,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.IOnDoneCallback;
-import androidx.car.app.ISearchListener;
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.Screen;
-import androidx.car.app.SearchListener;
 import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.host.SearchListenerWrapper;
-import androidx.car.app.utils.Logger;
 import androidx.car.app.utils.RemoteUtils;
 
 import java.util.Collections;
@@ -45,11 +41,34 @@
  *
  * <h4>Template Restrictions</h4>
  *
- * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template
+ * In regards to template refreshes, as described in {@link Screen#onGetTemplate()}, this template
  * supports any content changes as refreshes. This allows apps to interactively update the search
  * results as the user types without the templates being counted against the quota.
  */
 public final class SearchTemplate implements Template {
+
+    /** A listener for search updates. */
+    public interface SearchListener {
+        /**
+         * Notifies the current {@code searchText}.
+         *
+         * <p>The host may invoke this callback as the user types a search text. The frequency of
+         * these updates is not guaranteed to be after every individual keystroke. The host may
+         * decide to wait for several keystrokes before sending a single update.
+         *
+         * @param searchText the current search text that the user has typed.
+         */
+        void onSearchTextChanged(@NonNull String searchText);
+
+        /**
+         * Notifies that the user has submitted the search and the given {@code searchText} is
+         * the final term.
+         *
+         * @param searchText the search text that the user typed.
+         */
+        void onSearchSubmitted(@NonNull String searchText);
+    }
+
     @Keep
     private final boolean mIsLoading;
     @Keep
@@ -151,13 +170,6 @@
         return mShowKeyboardByDefault;
     }
 
-    @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        // Always allow updating on search templates. Search results needs to be updated on the fly
-        // as user searches.
-        return oldTemplate.getClass() == this.getClass();
-    }
-
     @NonNull
     @Override
     public String toString() {
@@ -347,8 +359,8 @@
          *
          * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
          * ignore any items over that limit. The list itself cannot be selectable as set via {@link
-         * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
-         * {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
+         * ItemList.Builder#setOnSelectedListener}. Each {@link Row} can add up to 2 lines of texts
+         * via {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
          *
          * @throws IllegalArgumentException if {@code itemList} does not meet the template's
          *                                  requirements.
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Template.java b/car/app/app/src/main/java/androidx/car/app/model/Template.java
index 46eb37a..20a8881 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Template.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Template.java
@@ -19,19 +19,9 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
-import androidx.car.app.utils.Logger;
 
 /** An interface used to denote a model that can act as a root for a tree of other models. */
 public interface Template {
-
-    /**
-     * Returns {@code true} if this {@link Template} instance is determined to be a refresh compared
-     * to the input template.
-     */
-    default boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        return false;
-    }
-
     /**
      * Checks that the application has the required permissions for this template.
      *
diff --git a/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java
index a26e151..21031ab 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/TemplateWrapper.java
@@ -50,8 +50,7 @@
 
     /**
      * Whether the template wrapper is a refresh of the current template. For internal, host-side
-     * use
-     * only.
+     * use only.
      */
     private boolean mIsRefresh;
 
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Toggle.java b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
index a58eb9d..c657aec 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
@@ -25,11 +25,9 @@
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.car.app.IOnCheckedChangeListener;
 import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnCheckedChangeListenerWrapper;
-import androidx.car.app.host.OnDoneCallback;
 import androidx.car.app.utils.RemoteUtils;
 
 /** Represents a toggle that can have either a checked or unchecked state. */
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
index 28b8051..ca81759 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowConstraints.java
@@ -16,12 +16,9 @@
 
 package androidx.car.app.model.constraints;
 
-import static androidx.car.app.model.Row.ROW_FLAG_SHOW_DIVIDERS;
-
 import androidx.annotation.NonNull;
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.Row;
-import androidx.car.app.model.Row.RowFlags;
 
 /**
  * Encapsulates the constraints to apply when rendering a {@link
@@ -57,7 +54,6 @@
     @NonNull
     public static final RowConstraints ROW_CONSTRAINTS_SIMPLE =
             RowConstraints.builder()
-                    .setFlagOverrides(ROW_FLAG_SHOW_DIVIDERS)
                     .setMaxActionsExclusive(0)
                     .setImageAllowed(true)
                     .setMaxTextLinesPerRow(2)
@@ -75,8 +71,6 @@
     private final boolean mIsImageAllowed;
     private final boolean mIsToggleAllowed;
     private final boolean mIsOnClickListenerAllowed;
-    @RowFlags
-    private final int mFlagOverrides;
     private final CarIconConstraints mCarIconConstraints;
 
     /**
@@ -120,15 +114,6 @@
         return mIsImageAllowed;
     }
 
-    /**
-     * The flags that will be forced on each row, on top of whatever flags come from the client
-     * side.
-     */
-    @RowFlags
-    public int getFlagOverrides() {
-        return mFlagOverrides;
-    }
-
     /** Returns the {@link CarIconConstraints} enforced for the row images. */
     @NonNull
     public CarIconConstraints getCarIconConstraints() {
@@ -173,7 +158,6 @@
         mMaxActionsExclusive = builder.mMaxActionsExclusive;
         mIsToggleAllowed = builder.mIsToggleAllowed;
         mIsImageAllowed = builder.mIsImageAllowed;
-        mFlagOverrides = builder.mFlagOverrides;
         mCarIconConstraints = builder.mCarIconConstraints;
     }
 
@@ -184,8 +168,6 @@
         private int mMaxTextLines = Integer.MAX_VALUE;
         private int mMaxActionsExclusive = Integer.MAX_VALUE;
         private boolean mIsImageAllowed = true;
-        @RowFlags
-        private int mFlagOverrides;
         private CarIconConstraints mCarIconConstraints = CarIconConstraints.UNCONSTRAINED;
 
         /** Sets whether the row can have a click listener associated with it. */
@@ -223,16 +205,6 @@
             return this;
         }
 
-        /**
-         * Sets the flags that will be forced on each row, on top of whatever flags come from
-         * the client side.
-         */
-        @NonNull
-        public Builder setFlagOverrides(@RowFlags int flagOverrides) {
-            this.mFlagOverrides = flagOverrides;
-            return this;
-        }
-
         /** Sets the {@link CarIconConstraints} enforced for the row images. */
         @NonNull
         public Builder setCarIconConstraints(@NonNull CarIconConstraints carIconConstraints) {
@@ -257,7 +229,6 @@
             mMaxActionsExclusive = constraints.mMaxActionsExclusive;
             mIsToggleAllowed = constraints.mIsToggleAllowed;
             mIsImageAllowed = constraints.mIsImageAllowed;
-            mFlagOverrides = constraints.mFlagOverrides;
             mCarIconConstraints = constraints.mCarIconConstraints;
         }
     }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
index 5d20a94..393e555 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
@@ -189,7 +189,7 @@
      * @throws IllegalArgumentException if the constraints are not met.
      */
     public void validateOrThrow(@NonNull Pane pane) {
-        ActionList actions = pane.getActionList();
+        ActionList actions = pane.getActions();
         if (actions != null && actions.getList().size() > mMaxActions) {
             throw new IllegalArgumentException(
                     "The number of actions on the pane exceeded the supported max of "
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
index fd22326..2b78cae 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
@@ -23,6 +23,8 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.SuppressLint;
+import android.os.Looper;
+import android.util.Log;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
@@ -37,6 +39,9 @@
 import androidx.car.app.serialization.Bundleable;
 import androidx.car.app.serialization.BundlerException;
 import androidx.car.app.utils.RemoteUtils;
+import androidx.core.content.ContextCompat;
+
+import java.util.concurrent.Executor;
 
 /**
  * Manager for communicating navigation related events with the host.
@@ -50,15 +55,20 @@
  * called.
  *
  * <p>Navigation apps must also register a {@link NavigationManagerListener} to handle callbacks to
- * {@link NavigationManagerListener#stopNavigation()} issued by the host.
+ * {@link NavigationManagerListener#onStopNavigation()} issued by the host.
  */
 public class NavigationManager {
-    private final INavigationManager.Stub mNavigationmanager;
+    private static final String TAG = "NavigationManager";
+
+    private final CarContext mCarContext;
+    private final INavigationManager.Stub mNavigationManager;
     private final HostDispatcher mHostDispatcher;
 
     // Guarded by main thread access.
     @Nullable
-    private NavigationManagerListener mListener;
+    private NavigationManagerListener mNavigationManagerListener;
+    @Nullable
+    private Executor mNavigationManagerListenerExecutor;
     private boolean mIsNavigating;
     private boolean mIsAutoDriveEnabled;
 
@@ -71,7 +81,7 @@
      * <p>This method should only be invoked once the navigation app has called {@link
      * #navigationStarted()}, or else the updates will be dropped by the host. Once the app has
      * called {@link #navigationEnded()} or received
-     * {@link NavigationManagerListener#stopNavigation()} it should stop sending updates.
+     * {@link NavigationManagerListener#onStopNavigation()} it should stop sending updates.
      *
      * <p>As the location changes, and in accordance with speed and rounded distance changes, the
      * {@link TravelEstimate}s in the provided {@link Trip} should be rebuilt and this method called
@@ -83,13 +93,15 @@
      * androidx.car.app.navigation.model.Maneuver}s of unknown type may be skipped while on other
      * displays the associated icon may be shown.
      *
+     * @param trip destination, steps, and trip estimates to be sent to the host
+     *
      * @throws HostException            if the call is invoked by an app that is not declared as
-     *                                  a navigation app in the manifest.
+     *                                  a navigation app in the manifest
      * @throws IllegalStateException    if the call occurs when navigation is not started. See
-     *                                  {@link #navigationStarted()} for more info.
+     *                                  {@link #navigationStarted()} for more info
      * @throws IllegalArgumentException if any of the destinations, steps, or trip position is
-     *                                  not well formed.
-     * @throws IllegalStateException    if the current thread is not the main thread.
+     *                                  not well formed
+     * @throws IllegalStateException    if the current thread is not the main thread
      */
     @MainThread
     public void updateTrip(@NonNull Trip trip) {
@@ -115,25 +127,59 @@
     }
 
     /**
-     * Sets a listener to start receiving navigation manager events, or {@code null} to clear the
-     * listener.
+     * Sets a listener to start receiving navigation manager events.
      *
-     * @throws IllegalStateException if {@code null} is passed in while navigation is started. See
+     * Note that the listener will be executed on the main thread using
+     * {@link Looper#getMainLooper()}. To specify the execution thread, use
+     * {@link #setNavigationManagerListener(Executor, NavigationManagerListener)}.
+     *
+     * @param listener the {@link NavigationManagerListener} to use
+     *
+     * @throws IllegalStateException if the current thread is not the main thread
+     */
+    @SuppressLint("ExecutorRegistration")
+    @MainThread
+    public void setNavigationManagerListener(@NonNull NavigationManagerListener listener) {
+        checkMainThread();
+        Executor executor = ContextCompat.getMainExecutor(mCarContext);
+        setNavigationManagerListener(executor, listener);
+    }
+
+    /**
+     * Sets a listener to start receiving navigation manager events.
+     *
+     * @param executor the executor which will be used for invoking the listener
+     * @param listener the {@link NavigationManagerListener} to use
+     *
+     * @throws IllegalStateException if the current thread is not the main thread.
+     */
+    @MainThread
+    public void setNavigationManagerListener(@NonNull /* @CallbackExecutor */ Executor executor,
+            @NonNull NavigationManagerListener listener) {
+        checkMainThread();
+
+        mNavigationManagerListenerExecutor = executor;
+        mNavigationManagerListener = listener;
+        if (mIsAutoDriveEnabled) {
+            onAutoDriveEnabled();
+        }
+    }
+
+    /**
+     * Clears the listener for receiving navigation manager events.
+     *
+     * @throws IllegalStateException if navigation is started. See
      *                               {@link #navigationStarted()} for more info.
      * @throws IllegalStateException if the current thread is not the main thread.
      */
-    // TODO(rampara): Add Executor parameter.
-    @SuppressLint("ExecutorRegistration")
     @MainThread
-    public void setListener(@Nullable NavigationManagerListener listener) {
+    public void clearNavigationManagerListener() {
         checkMainThread();
-        if (mIsNavigating && listener == null) {
+        if (mIsNavigating) {
             throw new IllegalStateException("Removing listener while navigating");
         }
-        this.mListener = listener;
-        if (mIsAutoDriveEnabled && listener != null) {
-            listener.onAutoDriveEnabled();
-        }
+        mNavigationManagerListenerExecutor = null;
+        mNavigationManagerListener = null;
     }
 
     /**
@@ -143,10 +189,11 @@
      * the host. The app must call this method to inform the system that it has started
      * navigation in response to user action.
      *
-     * <p>This function can only called if {@link #setListener(NavigationManagerListener)} has been
+     * <p>This function can only called if
+     * {@link #setNavigationManagerListener(NavigationManagerListener)} has been
      * called with a non-{@code null} value. The listener is required so that a signal to stop
      * navigation from the host can be handled using
-     * {@link NavigationManagerListener#stopNavigation()}.
+     * {@link NavigationManagerListener#onStopNavigation()}.
      *
      * <p>This method is idempotent.
      *
@@ -159,7 +206,7 @@
         if (mIsNavigating) {
             return;
         }
-        if (mListener == null) {
+        if (mNavigationManagerListener == null) {
             throw new IllegalStateException("No listener has been set");
         }
         mIsNavigating = true;
@@ -206,8 +253,9 @@
      */
     @RestrictTo(LIBRARY)
     @NonNull
-    public static NavigationManager create(@NonNull HostDispatcher hostDispatcher) {
-        return new NavigationManager(hostDispatcher);
+    public static NavigationManager create(@NonNull CarContext carContext,
+            @NonNull HostDispatcher hostDispatcher) {
+        return new NavigationManager(carContext, hostDispatcher);
     }
 
     /**
@@ -218,7 +266,7 @@
     @RestrictTo(LIBRARY)
     @NonNull
     public INavigationManager.Stub getIInterface() {
-        return mNavigationmanager;
+        return mNavigationManager;
     }
 
     /**
@@ -228,19 +276,20 @@
      */
     @RestrictTo(LIBRARY)
     @MainThread
-    public void stopNavigation() {
+    public void onStopNavigation() {
         checkMainThread();
         if (!mIsNavigating) {
             return;
         }
         mIsNavigating = false;
-        requireNonNull(mListener).stopNavigation();
+        requireNonNull(mNavigationManagerListenerExecutor).execute(() -> {
+            requireNonNull(mNavigationManagerListener).onStopNavigation();
+        });
     }
 
     /**
-     * Signifies that from this point, until {@link
-     * androidx.car.app.CarAppService#onCarAppFinished} is called, any navigation
-     * should automatically start driving to the destination as if the user was moving.
+     * Signifies that from this point, until {@link CarContext#finishCarApp()} is called, any
+     * navigation should automatically start driving to the destination as if the user was moving.
      *
      * <p>This is used in a testing environment, allowing testing the navigation app's navigation
      * capabilities without being in a car.
@@ -252,22 +301,30 @@
     public void onAutoDriveEnabled() {
         checkMainThread();
         mIsAutoDriveEnabled = true;
-        if (mListener != null) {
-            mListener.onAutoDriveEnabled();
+        if (mNavigationManagerListener != null) {
+            Log.d(TAG, "Executing onAutoDriveEnabled");
+            requireNonNull(mNavigationManagerListenerExecutor).execute(() -> {
+                mNavigationManagerListener.onAutoDriveEnabled();
+            });
+        } else {
+            Log.w(TAG, "NavigationManagerListener not set, skipping onAutoDriveEnabled");
         }
     }
 
     /** @hide */
     @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
     @SuppressWarnings({"methodref.receiver.bound.invalid"})
-    protected NavigationManager(@NonNull HostDispatcher hostDispatcher) {
-        this.mHostDispatcher = requireNonNull(hostDispatcher);
-        mNavigationmanager =
+    protected NavigationManager(@NonNull CarContext carContext,
+            @NonNull HostDispatcher hostDispatcher) {
+        mCarContext = carContext;
+        mHostDispatcher = requireNonNull(hostDispatcher);
+        mNavigationManager =
                 new INavigationManager.Stub() {
                     @Override
-                    public void stopNavigation(IOnDoneCallback callback) {
+                    public void onStopNavigation(IOnDoneCallback callback) {
                         RemoteUtils.dispatchHostCall(
-                                NavigationManager.this::stopNavigation, callback, "stopNavigation");
+                                NavigationManager.this::onStopNavigation, callback,
+                                "onStopNavigation");
                     }
                 };
     }
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
index ee4c29f..fa2475c 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
@@ -16,12 +16,11 @@
 
 package androidx.car.app.navigation;
 
-import android.annotation.SuppressLint;
-
+import androidx.car.app.CarContext;
 import androidx.car.app.navigation.model.Trip;
 
 /**
- * Listener of events from the {@link NavigationManager}.
+ * Listener for events from the {@link NavigationManager}.
  *
  * @see NavigationManager
  */
@@ -34,18 +33,13 @@
      * guidance, routing-related notifications, and updating trip information via {@link
      * NavigationManager#updateTrip(Trip)}.
      */
-
-    // TODO(rampara): Listener method names must follow the on<Something> style. Consider
-    //  onShouldStopNavigation.
-    @SuppressLint("CallbackMethodName")
-    void stopNavigation();
+    void onStopNavigation();
 
     /**
      * Notifies the app that, from this point onwards, when the user chooses to navigate to a
      * destination, the app should start simulating a drive towards that destination.
      *
-     * <p>This mode should remain active until {@link
-     * androidx.car.app.CarAppService#onCarAppFinished} is called.
+     * <p>This mode should remain active until {@link CarContext#finishCarApp()} is called.
      *
      * <p>This functionality is used to allow verifying the app's navigation capabilities without
      * being in an actual car.
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
index 8f1e1f0..d937e9c 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Maneuver.java
@@ -31,7 +31,7 @@
 import java.util.Objects;
 
 /** Information about a maneuver that the driver will be required to perform. */
-// TODO: Update when Embedded updates or a scheme for auto sync is established.
+// TODO(rasekh): Update when host(s) updates or a scheme for auto sync is established.
 public final class Maneuver {
     /**
      * Possible maneuver types.
@@ -81,14 +81,21 @@
             TYPE_DESTINATION,
             TYPE_DESTINATION_STRAIGHT,
             TYPE_DESTINATION_LEFT,
-            TYPE_DESTINATION_RIGHT
+            TYPE_DESTINATION_RIGHT,
+            TYPE_ROUNDABOUT_ENTER_CW,
+            TYPE_ROUNDABOUT_EXIT_CW,
+            TYPE_ROUNDABOUT_ENTER_CCW,
+            TYPE_ROUNDABOUT_EXIT_CCW,
+            TYPE_FERRY_BOAT_LEFT,
+            TYPE_FERRY_BOAT_RIGHT,
+            TYPE_FERRY_TRAIN_LEFT,
+            TYPE_FERRY_TRAIN_RIGHT,
     })
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(LIBRARY)
     public @interface Type {
     }
 
-    // LINT.IfChange(enums)
     /**
      * Maneuver type is unknown, no maneuver information should be displayed.
      *
@@ -286,7 +293,11 @@
      * Roundabout entrance on which the current road ends.
      *
      * <p>For example, this is used to indicate "Enter the roundabout".
+     *
+     * @deprecated Use {@link #TYPE_ROUNDABOUT_ENTER_CW} or {@link #TYPE_ROUNDABOUT_ENTER_CCW}
+     * instead.
      */
+    @Deprecated
     @Type
     public static final int TYPE_ROUNDABOUT_ENTER = 30;
 
@@ -294,7 +305,11 @@
      * Used when leaving a roundabout when the step starts in it.
      *
      * <p>For example, this is used to indicate "Exit the roundabout".
+     *
+     * @deprecated Use {@link #TYPE_ROUNDABOUT_EXIT_CW} or {@link #TYPE_ROUNDABOUT_EXIT_CCW}
+     * instead.
      */
+    @Deprecated
     @Type
     public static final int TYPE_ROUNDABOUT_EXIT = 31;
 
@@ -344,14 +359,18 @@
     public static final int TYPE_STRAIGHT = 36;
 
     /**
-     * Drive towards a boat ferry for vehicles.
+     * Drive towards a boat ferry for vehicles, where the entrance is straight ahead or in an
+     * unknown direction.
      *
      * <p>For example, this is used to indicate "Take the ferry".
      */
     @Type
     public static final int TYPE_FERRY_BOAT = 37;
 
-    /** Drive towards a train ferry for vehicles (e.g. "Take the train"). */
+    /**
+     * Drive towards a train ferry for vehicles (e.g. "Take the train"), where the entrance is
+     * straight ahead or in an unknown direction.
+     */
     @Type
     public static final int TYPE_FERRY_TRAIN = 38;
 
@@ -370,7 +389,70 @@
     /** Arrival to a destination located to the right side of the road. */
     @Type
     public static final int TYPE_DESTINATION_RIGHT = 42;
-    // LINT.ThenChange(:enumTypeChecks)
+
+    /**
+     * Entrance to a clockwise roundabout on which the current road ends.
+     *
+     * <p>For example, this is used to indicate "Enter the roundabout".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_ENTER_CW = 43;
+
+    /**
+     * Used when leaving a clockwise roundabout when the step starts in it.
+     *
+     * <p>For example, this is used to indicate "Exit the roundabout".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_EXIT_CW = 44;
+
+    /**
+     * Entrance to a counter-clockwise roundabout on which the current road ends.
+     *
+     * <p>For example, this is used to indicate "Enter the roundabout".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_ENTER_CCW = 45;
+
+    /**
+     * Used when leaving a counter-clockwise roundabout when the step starts in it.
+     *
+     * <p>For example, this is used to indicate "Exit the roundabout".
+     */
+    @Type
+    public static final int TYPE_ROUNDABOUT_EXIT_CCW = 46;
+
+    /**
+     * Drive towards a boat ferry for vehicles, where the entrance is to the left.
+     *
+     * <p>For example, this is used to indicate "Take the ferry".
+     */
+    @Type
+    public static final int TYPE_FERRY_BOAT_LEFT = 47;
+
+    /**
+     * Drive towards a boat ferry for vehicles, where the entrance is to the right.
+     *
+     * <p>For example, this is used to indicate "Take the ferry".
+     */
+    @Type
+    public static final int TYPE_FERRY_BOAT_RIGHT = 48;
+
+    /**
+     * Drive towards a train ferry for vehicles (e.g. "Take the train"), where the entrance is to
+     * the
+     * left.
+     */
+    @Type
+    public static final int TYPE_FERRY_TRAIN_LEFT = 49;
+
+    /**
+     * Drive towards a train ferry for vehicles (e.g. "Take the train"), where the entrance is to
+     * the
+     * right.
+     */
+    @Type
+    public static final int TYPE_FERRY_TRAIN_RIGHT = 50;
 
     @Keep
     @Type
@@ -388,7 +470,7 @@
      *
      * <p>The type should be chosen to reflect the closest semantic meaning of the maneuver. In some
      * cases, an exact type match is not possible, but choosing a similar or slightly more general
-     * type is preferred. Using {@link #TYPE_UNKNOWN} is allowed, but some headunits will not
+     * type is preferred. Using {@link #TYPE_UNKNOWN} is allowed, but some head units will not
      * display any information in that case.
      *
      * @param type one of the {@code TYPE_*} static constants defined in this class.
@@ -513,7 +595,7 @@
     }
 
     private static boolean isValidType(@Type int type) {
-        return (type >= TYPE_UNKNOWN && type <= TYPE_DESTINATION_RIGHT);
+        return (type >= TYPE_UNKNOWN && type <= TYPE_FERRY_TRAIN_RIGHT);
     }
 
     private static boolean isValidTypeWithExitNumber(@Type int type) {
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
index 22e6df5..b463033 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
@@ -33,7 +33,6 @@
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarColor;
 import androidx.car.app.model.Template;
-import androidx.car.app.utils.Logger;
 
 import java.util.Objects;
 
@@ -60,7 +59,7 @@
  *
  * <h4>Template Restrictions</h4>
  *
- * In regard to template refreshes, as described in {@link Screen#getTemplate()}, this template
+ * In regard to template refreshes, as described in {@link Screen#onGetTemplate()}, this template
  * supports any content changes as refreshes. This allows apps to interactively update the
  * turn-by-turn instructions without the templates being counted against the template quota.
  *
@@ -118,14 +117,6 @@
         return requireNonNull(mActionStrip);
     }
 
-    @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        requireNonNull(oldTemplate);
-
-        // Always allow updating on navigation templates.
-        return oldTemplate.getClass() == this.getClass();
-    }
-
     @NonNull
     @Override
     public String toString() {
@@ -217,10 +208,19 @@
         /**
          * Sets the {@link TravelEstimate} to the final destination, or {@code null} to not show any
          * travel estimate information.
+         *
+         * @throws IllegalArgumentException if the {@link TravelEstimate}'s remaining time is
+         *                                  less than zero.
          */
         @NonNull
         public Builder setDestinationTravelEstimate(
                 @Nullable TravelEstimate destinationTravelEstimate) {
+            if (destinationTravelEstimate != null
+                    && destinationTravelEstimate.getRemainingTimeSeconds() < 0) {
+                throw new IllegalArgumentException(
+                        "The destination travel estimate's remaining time must be greater or "
+                                + "equal to zero");
+            }
             this.mDestinationTravelEstimate = destinationTravelEstimate;
             return this;
         }
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
index 51a304d..3a267185 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
@@ -20,8 +20,6 @@
 import static androidx.car.app.model.constraints.ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE;
 import static androidx.car.app.model.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
 
-import static java.util.Objects.requireNonNull;
-
 import android.content.Context;
 
 import androidx.annotation.Keep;
@@ -42,7 +40,6 @@
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
 import androidx.car.app.model.Toggle;
-import androidx.car.app.utils.Logger;
 
 import java.util.Collections;
 import java.util.List;
@@ -56,12 +53,12 @@
  *
  * <h4>Template Restrictions</h4>
  *
- * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template is
- * considered a refresh of a previous one if:
+ * In regards to template refreshes, as described in {@link Screen#onGetTemplate()}, this template
+ * is considered a refresh of a previous one if:
  *
  * <ul>
  *   <li>The template title has not changed, and
- *   <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ *   <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
  *       number of rows and the string contents (title, texts, not counting spans) of each row
  *       between the previous and new {@link ItemList}s have not changed.
  * </ul>
@@ -116,29 +113,6 @@
     }
 
     @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        requireNonNull(oldTemplate);
-        if (oldTemplate.getClass() != this.getClass()) {
-            return false;
-        }
-
-        PlaceListNavigationTemplate old = (PlaceListNavigationTemplate) oldTemplate;
-        if (!Objects.equals(old.getTitle(), getTitle())) {
-            return false;
-        }
-
-        if (old.mIsLoading) {
-            // Transition from a previous loading state is allowed.
-            return true;
-        } else if (mIsLoading) {
-            // Transition to a loading state is disallowed.
-            return false;
-        }
-
-        return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
-    }
-
-    @Override
     public void checkPermissions(@NonNull Context context) {
         CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
     }
@@ -217,10 +191,8 @@
          * once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
          * contents added via {@link #setItemList}.
          */
-        // TODO(rampara): Consider renaming to setLoading()
-        @SuppressWarnings("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setIsLoading(boolean isLoading) {
+        public Builder setLoading(boolean isLoading) {
             this.mIsLoading = isLoading;
             return this;
         }
@@ -259,8 +231,8 @@
          *
          * This template allows up to 6 {@link Row}s in the {@link ItemList}. The host will
          * ignore any items over that limit. The list itself cannot be selectable as set via {@link
-         * ItemList.Builder#setSelectable}. Each {@link Row} can add up to 2 lines of texts via
-         * {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
+         * ItemList.Builder#setOnSelectedListener}. Each {@link Row} can add up to 2 lines of texts
+         * via {@link Row.Builder#addText} and cannot contain a {@link Toggle}.
          *
          * <p>Images of type {@link Row#IMAGE_TYPE_LARGE} are not allowed in this template.
          *
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
index aae7e95..9c0160b 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
@@ -40,7 +40,6 @@
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
 import androidx.car.app.model.Toggle;
-import androidx.car.app.utils.Logger;
 
 import java.util.Collections;
 import java.util.Objects;
@@ -49,7 +48,7 @@
  * A template that supports showing a list of routes alongside a custom drawn map.
  *
  * <p>The list must have its {@link
- * androidx.car.app.model.ItemList.OnSelectedListener} set, and the template
+ * ItemList.OnSelectedListener} set, and the template
  * must have its navigate action set (see {@link Builder#setNavigateAction}). These are used in
  * conjunction to inform the app that:
  *
@@ -64,12 +63,12 @@
  *
  * <h4>Template Restrictions</h4>
  *
- * In regards to template refreshes, as described in {@link Screen#getTemplate()}, this template is
- * considered a refresh of a previous one if:
+ * In regards to template refreshes, as described in {@link Screen#onGetTemplate()}, this template
+ * is considered a refresh of a previous one if:
  *
  * <ul>
  *   <li>The template title has not changed, and
- *   <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ *   <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
  *       number of rows and the string contents (title, texts, not counting spans) of each row
  *       between the previous and new {@link ItemList}s have not changed.
  * </ul>
@@ -135,28 +134,6 @@
     }
 
     @Override
-    public boolean isRefresh(@NonNull Template oldTemplate, @NonNull Logger logger) {
-        if (oldTemplate.getClass() != this.getClass()) {
-            return false;
-        }
-
-        RoutePreviewNavigationTemplate old = (RoutePreviewNavigationTemplate) oldTemplate;
-        if (!Objects.equals(old.getTitle(), getTitle())) {
-            return false;
-        }
-
-        if (old.mIsLoading) {
-            // Transition from a previous loading state is allowed.
-            return true;
-        } else if (mIsLoading) {
-            // Transition to a loading state is disallowed.
-            return false;
-        }
-
-        return requireNonNull(mItemList).isRefresh(old.getItemList(), logger);
-    }
-
-    @Override
     public void checkPermissions(@NonNull Context context) {
         CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
     }
@@ -241,10 +218,8 @@
          * once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
          * contents added via {@link #setItemList}.
          */
-        // TODO(rampara): Consider renaming to setLoading()
-        @SuppressWarnings("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setIsLoading(boolean isLoading) {
+        public Builder setLoading(boolean isLoading) {
             this.mIsLoading = isLoading;
             return this;
         }
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
index 1de529f..c5c2e67 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
@@ -214,10 +214,8 @@
          *
          * @see #build
          */
-        // TODO(rampara): Consider renaming to setLoading()
-        @SuppressWarnings("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setIsLoading(boolean isLoading) {
+        public Builder setLoading(boolean isLoading) {
             this.mIsLoading = isLoading;
             return this;
         }
@@ -228,7 +226,7 @@
          * <h4>Requirements</h4>
          *
          * The {@link RoutingInfo} can be in a loading state by passing {@code true} to {@link
-         * #setIsLoading(boolean)}, in which case no other fields may be set. Otherwise, the current
+         * #setLoading(boolean)}, in which case no other fields may be set. Otherwise, the current
          * step and distance must be set. If the lane information is set with {@link
          * Step.Builder#addLane(Lane)}, then the lane image must also be set with {@link
          * Step.Builder#setLanesImage(CarIcon)}.
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
index 7a1c100..25721ed 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
@@ -58,6 +58,9 @@
      * <p>A cue must always be set when the step is created and is used as a fallback when {@link
      * Maneuver} is not set or is unavailable.
      *
+     * <p>Some cluster displays do not support UTF-8 encoded characters, in which case unsupported
+     * characters will not be displayed properly.
+     *
      * @throws NullPointerException if {@code cue} is {@code null}.
      * @see Builder#setCue(CharSequence)
      */
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
index cda7135..8efd3c4 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
@@ -39,6 +39,9 @@
  */
 @SuppressWarnings("MissingSummary")
 public final class TravelEstimate {
+    /** A value used to represent an unknown remaining amount of time. */
+    public static final long REMAINING_TIME_UNKNOWN = -1L;
+
     @Keep
     @Nullable
     private final Distance mRemainingDistance;
@@ -59,7 +62,7 @@
      * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
      *                                 the destination.
      * @param remainingTimeSeconds     The estimated time remaining until arriving at the
-     *                                 destination, in seconds.
+     *                                 destination, in seconds, or {@link #REMAINING_TIME_UNKNOWN}.
      * @param arrivalTimeAtDestination The arrival time with the time zone information provided
      *                                 for the destination.
      * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value.
@@ -71,7 +74,8 @@
             @NonNull Distance remainingDistance,
             long remainingTimeSeconds,
             @NonNull DateTimeWithZone arrivalTimeAtDestination) {
-        return builder(remainingDistance, remainingTimeSeconds, arrivalTimeAtDestination).build();
+        return builder(remainingDistance, arrivalTimeAtDestination).setRemainingTimeSeconds(
+                remainingTimeSeconds).build();
     }
 
     /**
@@ -81,7 +85,8 @@
      * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
      *                                 the destination.
      * @param remainingTime            The estimated time remaining until arriving at the
-     *                                 destination.
+     *                                 destination, or {@code Duration.ofSeconds
+     *                                 (REMAINING_TIME_UNKNOWN)}.
      * @param arrivalTimeAtDestination The arrival time with the time zone information provided for
      *                                 the destination.
      * @throws IllegalArgumentException if {@code remainingTime} contains a negative duration.
@@ -96,7 +101,8 @@
             @NonNull Distance remainingDistance,
             @NonNull Duration remainingTime,
             @NonNull ZonedDateTime arrivalTimeAtDestination) {
-        return builder(remainingDistance, remainingTime, arrivalTimeAtDestination).build();
+        return builder(remainingDistance, arrivalTimeAtDestination).setRemainingTime(
+                remainingTime).build();
     }
 
     /**
@@ -104,22 +110,17 @@
      *
      * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
      *                                 the destination.
-     * @param remainingTimeSeconds     The estimated time remaining until arriving at the
-     *                                 destination, in seconds.
      * @param arrivalTimeAtDestination The arrival time with the time zone information provided
      *                                 for the destination.
-     * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value.
-     * @throws NullPointerException     if {@code remainingDistance} is {@code null}
-     * @throws NullPointerException     if {@code arrivalTimeAtDestination} is {@code null}
+     * @throws NullPointerException if {@code remainingDistance} is {@code null}
+     * @throws NullPointerException if {@code arrivalTimeAtDestination} is {@code null}
      */
     @NonNull
     public static Builder builder(
             @NonNull Distance remainingDistance,
-            long remainingTimeSeconds,
             @NonNull DateTimeWithZone arrivalTimeAtDestination) {
         return new Builder(
                 requireNonNull(remainingDistance),
-                remainingTimeSeconds,
                 requireNonNull(arrivalTimeAtDestination));
     }
 
@@ -128,25 +129,19 @@
      *
      * @param remainingDistance        The estimated remaining {@link Distance} until arriving at
      *                                 the destination.
-     * @param remainingTime            The estimated time remaining until arriving at the
-     *                                 destination.
      * @param arrivalTimeAtDestination The arrival time with the time zone information provided for
      *                                 the destination.
-     * @throws IllegalArgumentException if {@code remainingTime} contains a negative duration.
-     * @throws NullPointerException     if {@code remainingDistance} is {@code null}
-     * @throws NullPointerException     if {@code remainingTime} is {@code null}
-     * @throws NullPointerException     if {@code arrivalTimeAtDestination} is {@code null}
+     * @throws NullPointerException if {@code remainingDistance} is {@code null}
+     * @throws NullPointerException if {@code arrivalTimeAtDestination} is {@code null}
      */
     @NonNull
     @RequiresApi(26)
     @SuppressWarnings("AndroidJdkLibsChecker")
     public static Builder builder(
             @NonNull Distance remainingDistance,
-            @NonNull Duration remainingTime,
             @NonNull ZonedDateTime arrivalTimeAtDestination) {
         return new Builder(
                 requireNonNull(remainingDistance),
-                requireNonNull(remainingTime),
                 requireNonNull(arrivalTimeAtDestination));
     }
 
@@ -158,7 +153,7 @@
     // TODO(rampara): Returned time values must be in milliseconds
     @SuppressWarnings("MethodNameUnits")
     public long getRemainingTimeSeconds() {
-        return mRemainingTimeSeconds;
+        return mRemainingTimeSeconds >= 0 ? mRemainingTimeSeconds : REMAINING_TIME_UNKNOWN;
     }
 
     @Nullable
@@ -239,17 +234,15 @@
     /** A builder of {@link TravelEstimate}. */
     public static final class Builder {
         private final Distance mRemainingDistance;
-        private final long mRemainingTimeSeconds;
+        private long mRemainingTimeSeconds = REMAINING_TIME_UNKNOWN;
         private final DateTimeWithZone mArrivalTimeAtDestination;
         private CarColor mRemainingTimeColor = CarColor.DEFAULT;
         private CarColor mRemainingDistanceColor = CarColor.DEFAULT;
 
         private Builder(
                 Distance remainingDistance,
-                long remainingTimeSeconds,
                 DateTimeWithZone arrivalTimeAtDestination) {
             this.mRemainingDistance = requireNonNull(remainingDistance);
-            this.mRemainingTimeSeconds = validateRemainingTime(remainingTimeSeconds);
             this.mArrivalTimeAtDestination = requireNonNull(arrivalTimeAtDestination);
         }
 
@@ -259,14 +252,46 @@
         @SuppressWarnings("AndroidJdkLibsChecker")
         private Builder(
                 Distance remainingDistance,
-                Duration remainingTime,
                 ZonedDateTime arrivalTimeAtDestination) {
             this.mRemainingDistance = remainingDistance;
-            this.mRemainingTimeSeconds = validateRemainingTime(remainingTime.getSeconds());
             this.mArrivalTimeAtDestination = DateTimeWithZone.create(arrivalTimeAtDestination);
         }
 
         /**
+         * Sets the estimated time remaining until arriving at the destination, in seconds.
+         *
+         * <p>If not set, {@link #REMAINING_TIME_UNKNOWN} will be used.
+         *
+         * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value
+         *                                  but not {@link #REMAINING_TIME_UNKNOWN}.
+         */
+        @NonNull
+        public Builder setRemainingTimeSeconds(long remainingTimeSeconds) {
+            this.mRemainingTimeSeconds = validateRemainingTime(remainingTimeSeconds);
+            return this;
+        }
+
+        /**
+         * Sets the estimated time remaining until arriving at the destination.
+         *
+         * <p>If not set, {@link #REMAINING_TIME_UNKNOWN} will be used.
+         *
+         * @throws IllegalArgumentException if {@code remainingTime} is a negative duration
+         *                                  but not {@link #REMAINING_TIME_UNKNOWN}.
+         * @throws NullPointerException     if {@code remainingTime} is {@code null}
+         */
+        @SuppressLint({"MissingGetterMatchingBuilder", "UnsafeNewApiCall"})
+        // TODO(rampara): Move API 26 calls into separate class.
+        @RequiresApi(26)
+        @SuppressWarnings("AndroidJdkLibsChecker")
+        @NonNull
+        public Builder setRemainingTime(@NonNull Duration remainingTime) {
+            requireNonNull(remainingTime);
+            this.mRemainingTimeSeconds = validateRemainingTime(remainingTime.getSeconds());
+            return this;
+        }
+
+        /**
          * Sets the color of the remaining time text.
          *
          * <p>The host may ignore this color depending on the capabilities of the target screen.
@@ -312,9 +337,10 @@
         }
 
         private static long validateRemainingTime(long remainingTimeSeconds) {
-            if (remainingTimeSeconds < 0) {
+            if (remainingTimeSeconds < 0 && remainingTimeSeconds != REMAINING_TIME_UNKNOWN) {
                 throw new IllegalArgumentException(
-                        "Remaining time must be a larger than or equal to zero");
+                        "Remaining time must be a larger than or equal to zero, or set to"
+                                + " REMAINING_TIME_UNKNOWN");
             }
             return remainingTimeSeconds;
         }
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
index d3f7b5d..15615f2 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
@@ -267,10 +267,8 @@
          * <p>If set to {@code true}, the UI may show a loading indicator, and adding any steps
          * or step travel estimates will throw an {@link IllegalArgumentException}.
          */
-        // TODO(rampara): Consider renaming to setLoading()
-        @SuppressWarnings("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setIsLoading(boolean isLoading) {
+        public Builder setLoading(boolean isLoading) {
             this.mIsLoading = isLoading;
             return this;
         }
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
index 05fc5a1..dc259cc 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
@@ -278,7 +278,8 @@
      *
      * @see Builder#setSmallIcon(int)
      */
-    public int getSmallIconResId() {
+    @DrawableRes
+    public int getSmallIcon() {
         return mSmallIconResId;
     }
 
@@ -288,7 +289,7 @@
      * @see Builder#setLargeIcon(Bitmap)
      */
     @Nullable
-    public Bitmap getLargeIconBitmap() {
+    public Bitmap getLargeIcon() {
         return mLargeIconBitmap;
     }
 
@@ -391,8 +392,6 @@
          * <p>This method is equivalent to {@link NotificationCompat.Builder#setSmallIcon(int)} for
          * the car screen.
          */
-        // TODO(rampara): Revisit small icon getter API
-        @SuppressWarnings("MissingGetterMatchingBuilder")
         @NonNull
         public Builder setSmallIcon(int iconResId) {
             this.mSmallIconResId = iconResId;
@@ -412,8 +411,6 @@
          * <p>The large icon will be shown in the notification badge. If the large icon is not
          * set in the {@link CarAppExtender} or the notification, the small icon will show instead.
          */
-        // TODO(rampara): Revisit small icon getter API
-        @SuppressWarnings("MissingGetterMatchingBuilder")
         @NonNull
         public Builder setLargeIcon(@Nullable Bitmap bitmap) {
             this.mLargeIconBitmap = bitmap;
@@ -494,13 +491,6 @@
             return this;
         }
 
-        /** Clears any actions that may have been added with {@link #addAction} up to this point. */
-        @NonNull
-        public Builder clearActions() {
-            this.mActions.clear();
-            return this;
-        }
-
         /**
          * Sets the importance of the notification in the car screen.
          *
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
index ba59fcc..09d87e2 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
@@ -31,10 +31,10 @@
 import androidx.car.app.HostException;
 import androidx.car.app.IOnDoneCallback;
 import androidx.car.app.ISurfaceListener;
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.SurfaceContainer;
 import androidx.car.app.SurfaceListener;
 import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnDoneCallback;
 import androidx.car.app.serialization.Bundleable;
 import androidx.car.app.serialization.BundlerException;
 
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
deleted file mode 100644
index 3e4a283..0000000
--- a/car/app/app/src/main/java/androidx/car/app/utils/ValidationUtils.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.car.app.utils;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.car.app.model.CarText;
-import androidx.car.app.model.GridItem;
-import androidx.car.app.model.Row;
-import androidx.car.app.model.Toggle;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Shared util methods for handling different distraction validation logic.
- *
- * @hide
- */
-@RestrictTo(LIBRARY)
-public class ValidationUtils {
-    private static final int INVALID_INDEX = -1;
-
-    /**
-     * Returns {@code true} if the sizes and string contents of the two lists of items are equal,
-     * {@code false} otherwise.
-     */
-    public static boolean itemsHaveSameContent(
-            @NonNull List<Object> itemList1, @NonNull List<Object> itemList2,
-            @NonNull Logger logger) {
-        return itemsHaveSameContent(itemList1, INVALID_INDEX, itemList2, INVALID_INDEX, logger);
-    }
-
-    /**
-     * Returns {@code true} if the sizes and string contents of the two lists of items are equal,
-     * {@code false} otherwise.
-     */
-    public static boolean itemsHaveSameContent(
-            @NonNull List<Object> itemList1,
-            int itemList1SelectedIndex,
-            @NonNull List<Object> itemList2,
-            int itemList2SelectedIndex,
-            @NonNull Logger logger) {
-        if (itemList1.size() != itemList2.size()) {
-            logger.log(
-                    "Different item list sizes. Old: " + itemList1.size() + ". New: "
-                            + itemList2.size());
-            return false;
-        }
-
-        for (int i = 0; i < itemList1.size(); i++) {
-            Object itemObj1 = itemList1.get(i);
-            Object itemObj2 = itemList2.get(i);
-
-            if (itemObj1.getClass() != itemObj2.getClass()) {
-                logger.log(
-                        "Different item types at index "
-                                + i
-                                + ". Old: "
-                                + itemObj1.getClass()
-                                + ". New: "
-                                + itemObj2.getClass());
-                return false;
-            }
-
-            if (itemObj1 instanceof Row) {
-                if (!rowsHaveSameContent((Row) itemObj1, (Row) itemObj2, i, logger)) {
-                    return false;
-                }
-            } else if (itemObj1 instanceof GridItem) {
-                if (!gridItemsHaveSameContent(
-                        (GridItem) itemObj1,
-                        itemList1SelectedIndex == i,
-                        (GridItem) itemObj2,
-                        itemList2SelectedIndex == i,
-                        i,
-                        logger)) {
-                    return false;
-                }
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns {@code true} if the string contents of the two rows are equal, {@code false}
-     * otherwise.
-     */
-    private static boolean rowsHaveSameContent(Row row1, Row row2, int index, Logger logger) {
-        // Special case for rows with toggles - if the toggle state has changed, then text updates
-        // are allowed.
-        if (rowToggleStateHasChanged(row1, row2)) {
-            return true;
-        }
-
-        if (!carTextsHasSameString(row1.getTitle(), row2.getTitle())) {
-            logger.log(
-                    "Different row titles at index "
-                            + index
-                            + ". Old: "
-                            + row1.getTitle()
-                            + ". New: "
-                            + row2.getTitle());
-            return false;
-        }
-
-        List<CarText> row1Texts = row1.getTexts();
-        List<CarText> row2Texts = row2.getTexts();
-        if (row1Texts.size() != row2Texts.size()) {
-            logger.log(
-                    "Different text list size at row index "
-                            + index
-                            + ". Old: "
-                            + row1Texts.size()
-                            + ". New: "
-                            + row2Texts.size());
-            return false;
-        }
-
-        for (int j = 0; j < row1Texts.size(); j++) {
-            if (!carTextsHasSameString(row1Texts.get(j), row2Texts.get(j))) {
-                logger.log(
-                        "Different texts at row index "
-                                + index
-                                + ". Old row: "
-                                + row1Texts.get(j)
-                                + ". New row: "
-                                + row2Texts.get(j));
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns {@code true} if string contents and images of the two grid items are equal, {@code
-     * false} otherwise.
-     */
-    private static boolean gridItemsHaveSameContent(
-            GridItem gridItem1,
-            boolean isGridItem1Selected,
-            GridItem gridItem2,
-            boolean isGridItem2Selected,
-            int index,
-            Logger logger) {
-        // Special case for grid items with toggles - if the toggle state has changed, then text
-        // and image updates are allowed.
-        if (gridItemToggleStateHasChanged(gridItem1, gridItem2)) {
-            return true;
-        }
-
-        // Special case for grid items that are selectable - if the selected state has changed,
-        // then text and image updates are allowed.
-        if (isGridItem1Selected != isGridItem2Selected) {
-            return true;
-        }
-
-        if (!carTextsHasSameString(gridItem1.getTitle(), gridItem2.getTitle())) {
-            logger.log(
-                    "Different grid item titles at index "
-                            + index
-                            + ". Old: "
-                            + gridItem1.getTitle()
-                            + ". New: "
-                            + gridItem2.getTitle());
-            return false;
-        }
-
-        if (!carTextsHasSameString(gridItem1.getText(), gridItem2.getText())) {
-            logger.log(
-                    "Different grid item texts at index "
-                            + index
-                            + ". Old: "
-                            + gridItem1.getText()
-                            + ". New: "
-                            + gridItem2.getText());
-            return false;
-        }
-
-        if (!Objects.equals(gridItem1.getImage(), gridItem2.getImage())) {
-            logger.log("Different grid item images at index " + index);
-            return false;
-        }
-
-        if (gridItem1.getImageType() != gridItem2.getImageType()) {
-            logger.log(
-                    "Different grid item image types at index "
-                            + index
-                            + ". Old: "
-                            + gridItem1.getImageType()
-                            + ". New: "
-                            + gridItem2.getImageType());
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns {@code true} if the strings of the two {@link CarText}s are the same, {@code false}
-     * otherwise.
-     *
-     * <p>Spans that are attached to the strings are ignored from the comparison.
-     */
-    private static boolean carTextsHasSameString(
-            @Nullable CarText carText1, @Nullable CarText carText2) {
-        // If both carText1 and carText2 are null, return true. If only one of them is null,
-        // return false.
-        if (carText1 == null || carText2 == null) {
-            return carText1 == null && carText2 == null;
-        }
-
-        return Objects.equals(carText1.getText(), carText2.getText());
-    }
-
-    private static boolean rowToggleStateHasChanged(Row row1, Row row2) {
-        Toggle toggle1 = row1.getToggle();
-        Toggle toggle2 = row2.getToggle();
-
-        return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
-    }
-
-    private static boolean gridItemToggleStateHasChanged(GridItem gridItem1, GridItem gridItem2) {
-        Toggle toggle1 = gridItem1.getToggle();
-        Toggle toggle2 = gridItem2.getToggle();
-
-        return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
-    }
-
-    private ValidationUtils() {
-    }
-}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevel.java
similarity index 64%
copy from car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
copy to car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevel.java
index 2896a83..0da9df6 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevel.java
@@ -14,11 +14,17 @@
  * limitations under the License.
  */
 
-package androidx.car.app;
+package androidx.car.app.versioning;
 
-import androidx.car.app.IOnDoneCallback;
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /** @hide */
-oneway interface IOnSelectedListener {
-  void onSelected(int index, IOnDoneCallback callback) = 1;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@IntDef(value = {CarAppApiLevels.LEVEL_1})
+@Retention(RetentionPolicy.SOURCE)
+public @interface CarAppApiLevel {
 }
diff --git a/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java
new file mode 100644
index 0000000..21c453b
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.versioning;
+
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarContext;
+
+/**
+ * API levels supported by this library.
+ * <p>
+ * Each level denotes a set of elements (classes, fields and methods) known to both clients and
+ * hosts.
+ *
+ * @see CarContext#getCarAppApiLevel()
+ */
+public class CarAppApiLevels {
+
+    /**
+     * Initial API level.
+     * <p>
+     * Includes core API services and managers, and templates for parking,
+     * charging, and navigation apps.
+     */
+    @CarAppApiLevel
+    public static final int LEVEL_1 = 1;
+
+    /**
+     * Lowest API level implement to this library
+     */
+    @CarAppApiLevel
+    public static final int OLDEST = LEVEL_1;
+
+    /**
+     * Highest API level implemented by this library.
+     */
+    @CarAppApiLevel
+    public static final int LATEST = LEVEL_1;
+
+    /**
+     * Unknown API level. Used when the API level hasn't been established yet
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @CarAppApiLevel
+    public static final int UNKNOWN = 0;
+
+    /**
+     * @return true if the given integer is a valid {@link CarAppApiLevel}
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static boolean isValid(int carApiLevel) {
+        return carApiLevel >= OLDEST && carApiLevel <= LATEST;
+    }
+
+    private CarAppApiLevels() {}
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java b/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java
new file mode 100644
index 0000000..8b6f797
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+
+import androidx.car.app.versioning.CarAppApiLevels;
+
+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 org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class AppInfoTest {
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+    private final ApplicationInfo mApplicationInfo = new ApplicationInfo();
+
+    @Before
+    public void setUp() throws PackageManager.NameNotFoundException {
+        MockitoAnnotations.initMocks(this);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getApplicationInfo(isNull(), anyInt()))
+                .thenReturn(mApplicationInfo);
+    }
+
+    @Test
+    public void create_minApiLevel_defaultsToCurrent() {
+        mApplicationInfo.metaData = null;
+        AppInfo appInfo = AppInfo.create(mContext);
+        assertThat(appInfo.getMinCarAppApiLevel()).isEqualTo(CarAppApiLevels.LATEST);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void create_minApiLevel_cannotBeLowerThanOldest() {
+        int minApiLevel = CarAppApiLevels.OLDEST - 1;
+        mApplicationInfo.metaData = new Bundle();
+        mApplicationInfo.metaData.putInt(AppInfo.MIN_API_LEVEL_MANIFEST_KEY, minApiLevel);
+        AppInfo.create(mContext);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void create_minApiLevel_cannotBeHigherThanLatest() {
+        int minApiLevel = CarAppApiLevels.LATEST + 1;
+        mApplicationInfo.metaData = new Bundle();
+        mApplicationInfo.metaData.putInt(AppInfo.MIN_API_LEVEL_MANIFEST_KEY, minApiLevel);
+        AppInfo.create(mContext);
+    }
+
+    @Test
+    public void retrieveMinApiLevel_isReadFromManifest() {
+        int minApiLevel = 123;
+        mApplicationInfo.metaData = new Bundle();
+        mApplicationInfo.metaData.putInt(AppInfo.MIN_API_LEVEL_MANIFEST_KEY, minApiLevel);
+        assertThat(AppInfo.retrieveMinCarAppApiLevel(mContext)).isEqualTo(minApiLevel);
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/AppManagerTest.java b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
similarity index 94%
rename from car/app/app/src/androidTest/java/androidx/car/app/AppManagerTest.java
rename to car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
index 7b1f8bc..57e4c35 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/AppManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
@@ -30,20 +30,19 @@
 import androidx.annotation.Nullable;
 import androidx.car.app.model.Template;
 import androidx.car.app.testing.TestCarContext;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-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 org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link AppManager}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public final class AppManagerTest {
     @Mock
     private ICarHost mMockCarHost;
@@ -58,7 +57,6 @@
     private AppManager mAppManager;
 
     @Before
-    @UiThreadTest
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
 
@@ -92,7 +90,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void getTemplate_serializationFails_throwsIllegalStateException()
             throws RemoteException {
         mTestCarContext
@@ -100,7 +97,7 @@
                 .push(new Screen(mTestCarContext) {
                     @NonNull
                     @Override
-                    public Template getTemplate() {
+                    public Template onGetTemplate() {
                         return new Template() {
                         };
                     }
@@ -113,7 +110,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void invalidate_forwardsRequestToHost() throws RemoteException {
         mAppManager.invalidate();
 
@@ -121,7 +117,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void invalidate_hostThrowsRemoteException_throwsHostException() throws
             RemoteException {
         doThrow(new RemoteException()).when(mMockAppHost).invalidate();
@@ -130,7 +125,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void invalidate_hostThrowsRuntimeException_throwsHostException() throws
             RemoteException {
         doThrow(new IllegalStateException()).when(mMockAppHost).invalidate();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarAppServiceTest.java b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
similarity index 63%
rename from car/app/app/src/androidTest/java/androidx/car/app/CarAppServiceTest.java
rename to car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
index 98ee04a..d5a80dd 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarAppServiceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
@@ -35,25 +35,27 @@
 import androidx.car.app.serialization.BundlerException;
 import androidx.car.app.testing.CarAppServiceController;
 import androidx.car.app.testing.TestCarContext;
+import androidx.car.app.versioning.CarAppApiLevels;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.Lifecycle;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.Deque;
 import java.util.Locale;
 
 /** Tests for {@link CarAppService}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public final class CarAppServiceTest {
     @Mock
     ICarHost mMockCarHost;
@@ -68,36 +70,24 @@
                     .build();
 
     private CarAppService mCarAppService;
-
+    private CarAppServiceController mCarAppServiceController;
     private Intent mIntentSet;
-    private boolean mHasCarAppFinished;
+    @Captor
+    ArgumentCaptor<Bundleable> mBundleableArgumentCaptor;
 
     @Before
-    @UiThreadTest
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-
         mCarContext = TestCarContext.createCarContext(
                 ApplicationProvider.getApplicationContext());
-
         mCarAppService =
                 new CarAppService() {
                     @Override
                     @NonNull
-                    public Screen onCreateScreen(@NonNull Intent intent) {
-                        mIntentSet = intent;
-                        return new Screen(getCarContext()) {
-                            @Override
-                            @NonNull
-                            public Template getTemplate() {
-                                return mTemplate;
-                            }
-                        };
-                    }
-
-                    @Override
-                    public void onCarAppFinished() {
-                        mHasCarAppFinished = true;
+                    public Session onCreateSession() {
+                        Session testSession = createTestSession();
+                        CarAppServiceController.of(mCarContext, testSession, mCarAppService);
+                        return testSession;
                     }
 
                     @Override
@@ -106,18 +96,36 @@
                     }
                 };
 
-        CarAppServiceController.of(mCarContext, mCarAppService);
         mCarAppService.onCreate();
+        mCarAppServiceController = CarAppServiceController.of(mCarContext, createTestSession(),
+                mCarAppService);
+    }
+
+    private Session createTestSession() {
+        return new Session() {
+            @NonNull
+            @Override
+            public Screen onCreateScreen(@NonNull Intent intent) {
+                mIntentSet = intent;
+                return new Screen(getCarContext()) {
+                    @Override
+                    @NonNull
+                    public Template onGetTemplate() {
+                        return mTemplate;
+                    }
+                };
+            }
+        };
     }
 
     @Test
-    @UiThreadTest
     public void onAppCreate_createsFirstScreen() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
 
         assertThat(
                 mCarAppService
+                        .getCurrentSession()
                         .getCarContext()
                         .getCarService(ScreenManager.class)
                         .getTopTemplate()
@@ -126,7 +134,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void onAppCreate_withIntent_callsWithOnCreateScreenWithIntent() throws
             RemoteException {
         IOnDoneCallback callback = mock(IOnDoneCallback.class);
@@ -139,7 +146,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void onAppCreate_alreadyPreviouslyCreated_callsOnNewIntent() throws RemoteException {
         IOnDoneCallback callback = mock(IOnDoneCallback.class);
 
@@ -157,7 +163,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void onAppCreate_updatesTheConfiguration() throws RemoteException {
         Configuration configuration = new Configuration();
         configuration.setToDefaults();
@@ -171,7 +176,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void onNewIntent_callsOnNewIntentWithIntent() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         Intent intent = new Intent("Foo");
@@ -188,12 +192,11 @@
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
 
-        assertThat(
-                mCarAppService.getCarContext().getCarService(NavigationManager.class)).isNotNull();
+        assertThat(mCarAppService.getCurrentSession().getCarContext().getCarService(
+                NavigationManager.class)).isNotNull();
     }
 
     @Test
-    @UiThreadTest
     public void onConfigurationChanged_updatesTheConfiguration() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
@@ -209,61 +212,119 @@
     }
 
     @Test
+    public void getAppInfo() throws RemoteException, BundlerException {
+        AppInfo appInfo = new AppInfo(3, 4, "foo");
+        mCarAppServiceController.setAppInfo(appInfo);
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        IOnDoneCallback callback = mock(IOnDoneCallback.class);
+
+        carApp.getAppInfo(callback);
+
+        verify(callback).onSuccess(mBundleableArgumentCaptor.capture());
+        AppInfo receivedAppInfo = (AppInfo) mBundleableArgumentCaptor.getValue().get();
+        assertThat(receivedAppInfo.getMinCarAppApiLevel())
+                .isEqualTo(appInfo.getMinCarAppApiLevel());
+        assertThat(receivedAppInfo.getLatestCarAppApiLevel())
+                .isEqualTo(appInfo.getLatestCarAppApiLevel());
+        assertThat(receivedAppInfo.getLibraryVersion()).isEqualTo(appInfo.getLibraryVersion());
+    }
+
+    @Test
     public void onHandshakeCompleted_updatesHostInfo() throws RemoteException, BundlerException {
         String hostPackageName = "com.google.projection.gearhead";
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
-        HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName);
-        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback
-                .class));
+        HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, CarAppApiLevels.LEVEL_1);
+
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+
         assertThat(mCarAppService.getHostInfo().getPackageName()).isEqualTo(hostPackageName);
     }
 
     @Test
-    @UiThreadTest
-    public void onUnbind_movesLifecycleStateToStopped() throws RemoteException {
+    public void onHandshakeCompleted_updatesCarApiLevel() throws RemoteException, BundlerException {
+        String hostPackageName = "com.google.projection.gearhead";
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+        int hostApiLevel = CarAppApiLevels.LEVEL_1;
+        HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
+
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+
+        assertThat(
+                mCarAppService.getCurrentSession().getCarContext().getCarAppApiLevel()).isEqualTo(
+                hostApiLevel);
+    }
+
+    @Test
+    public void onHandshakeCompleted_lowerThanMinApiLevel_throws() throws BundlerException,
+            RemoteException {
+        AppInfo appInfo = new AppInfo(3, 4, "foo");
+        mCarAppServiceController.setAppInfo(appInfo);
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
+                appInfo.getMinCarAppApiLevel() - 1);
+        IOnDoneCallback callback = mock(IOnDoneCallback.class);
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+
+        verify(callback).onFailure(any());
+    }
+
+    @Test
+    public void onHandshakeCompleted_higherThanCurrentApiLevel_throws() throws BundlerException,
+            RemoteException {
+        AppInfo appInfo = new AppInfo(3, 4, "foo");
+        mCarAppServiceController.setAppInfo(appInfo);
+        ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+        HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
+                appInfo.getLatestCarAppApiLevel() + 1);
+        IOnDoneCallback callback = mock(IOnDoneCallback.class);
+        carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+
+        verify(callback).onFailure(any());
+    }
+
+    @Test
+    public void onUnbind_movesLifecycleStateToDestroyed() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
         carApp.onAppStart(mock(IOnDoneCallback.class));
 
-        mCarAppService.getLifecycle().addObserver(mLifecycleObserver);
+        mCarAppService.getCurrentSession().getLifecycle().addObserver(mLifecycleObserver);
 
         assertThat(mCarAppService.onUnbind(null)).isTrue();
 
-        verify(mLifecycleObserver).onStop(any());
+        verify(mLifecycleObserver).onDestroy(any());
     }
 
     @Test
-    @UiThreadTest
     public void onUnbind_rebind_callsOnCreateScreen() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
         carApp.onAppStart(mock(IOnDoneCallback.class));
 
-        mCarAppService.getLifecycle().addObserver(mLifecycleObserver);
-
+        Session currentSession = mCarAppService.getCurrentSession();
+        currentSession.getLifecycle().addObserver(mLifecycleObserver);
         assertThat(mCarAppService.onUnbind(null)).isTrue();
-        assertThat(mHasCarAppFinished).isTrue();
 
         verify(mLifecycleObserver).onStop(any());
 
-        assertThat(
-                mCarAppService.getCarContext().getCarService(ScreenManager.class).getScreenStack())
-                .isEmpty();
+        assertThat(currentSession.getCarContext().getCarService(
+                ScreenManager.class).getScreenStack()).isEmpty();
 
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
-        assertThat(
-                mCarAppService.getCarContext().getCarService(ScreenManager.class).getScreenStack())
-                .hasSize(1);
+        assertThat(currentSession.getCarContext().getCarService(
+                ScreenManager.class).getScreenStack()).hasSize(1);
     }
 
     @Test
-    @UiThreadTest
     public void onUnbind_clearsScreenStack() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
 
         Deque<Screen> screenStack =
-                mCarAppService.getCarContext().getCarService(ScreenManager.class).getScreenStack();
+                mCarAppService.getCurrentSession().getCarContext().getCarService(
+                        ScreenManager.class).getScreenStack();
         assertThat(screenStack).hasSize(1);
 
         Screen screen = screenStack.getFirst();
@@ -273,16 +334,14 @@
 
         assertThat(screenStack).isEmpty();
         assertThat(screen.getLifecycle().getCurrentState()).isEqualTo(Lifecycle.State.DESTROYED);
-        assertThat(mHasCarAppFinished).isTrue();
     }
 
     @Test
-    @UiThreadTest
     public void finish() throws RemoteException {
         ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
         carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
 
-        mCarAppService.finish();
+        mCarAppService.getCurrentSession().getCarContext().finishCarApp();
 
         assertThat(mCarContext.hasCalledFinishCarApp()).isTrue();
     }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarContextTest.java b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
similarity index 73%
rename from car/app/app/src/androidTest/java/androidx/car/app/CarContextTest.java
rename to car/app/app/src/test/java/androidx/car/app/CarContextTest.java
index ba52f42..3db83de 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarContextTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
@@ -21,6 +21,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -35,31 +36,31 @@
 import android.os.RemoteException;
 import android.util.DisplayMetrics;
 
+import androidx.activity.OnBackPressedCallback;
 import androidx.annotation.Nullable;
 import androidx.car.app.navigation.NavigationManager;
-import androidx.car.app.test.R;
 import androidx.car.app.testing.TestLifecycleOwner;
 import androidx.lifecycle.Lifecycle.Event;
-import androidx.test.annotation.UiThreadTest;
+import androidx.lifecycle.Lifecycle.State;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-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 org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.Locale;
 
 /** Tests for {@link CarContext}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class CarContextTest {
-    private static final String APP_SERVICE = "app_manager";
-    private static final String NAVIGATION_SERVICE = "navigation_manager";
-    private static final String SCREEN_MANAGER_SERVICE = "screen_manager";
+    private static final String APP_SERVICE = "app";
+    private static final String NAVIGATION_SERVICE = "navigation";
+    private static final String SCREEN_SERVICE = "screen";
 
     @Mock
     private ICarHost mMockCarHost;
@@ -79,7 +80,6 @@
             new TestLifecycleOwner();
 
     @Before
-    @UiThreadTest
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         when(mMockCarHost.getHost(CarContext.APP_SERVICE))
@@ -101,7 +101,7 @@
         TestStartCarAppStub startCarAppStub = new TestStartCarAppStub(mMockStartCarApp);
 
         Bundle extras = new Bundle(1);
-        extras.putBinder(CarContext.START_CAR_APP_BINDER_KEY, startCarAppStub.asBinder());
+        extras.putBinder(CarContext.EXTRA_START_CAR_APP_BINDER_KEY, startCarAppStub.asBinder());
         mIntentFromNotification = new Intent().putExtras(extras);
 
         mCarContext = CarContext.create(mLifecycleOwner.mRegistry);
@@ -130,9 +130,9 @@
 
     @Test
     public void getCarService_screenManager() {
-        assertThat(mCarContext.getCarService(CarContext.SCREEN_MANAGER_SERVICE))
+        assertThat(mCarContext.getCarService(CarContext.SCREEN_SERVICE))
                 .isEqualTo(mCarContext.getCarService(ScreenManager.class));
-        assertThat(mCarContext.getCarService(CarContext.SCREEN_MANAGER_SERVICE)).isNotNull();
+        assertThat(mCarContext.getCarService(CarContext.SCREEN_SERVICE)).isNotNull();
     }
 
     @Test
@@ -161,7 +161,7 @@
     @Test
     public void getCarServiceName_screenManager() {
         assertThat(mCarContext.getCarServiceName(ScreenManager.class)).isEqualTo(
-                SCREEN_MANAGER_SERVICE);
+                SCREEN_SERVICE);
     }
 
     @Test
@@ -210,7 +210,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void onConfigurationChanged_updatesTheConfiguration() {
         Configuration configuration = new Configuration();
         configuration.setToDefaults();
@@ -223,14 +222,13 @@
     }
 
     @Test
-    @UiThreadTest
     public void onConfigurationChanged_loadsCorrectNewResource() {
         Configuration ldpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
         ldpiConfig.densityDpi = 120;
 
         mCarContext.onCarConfigurationChanged(ldpiConfig);
 
-        Drawable ldpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+        Drawable ldpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
         assertThat(ldpiDrawable.getIntrinsicHeight()).isEqualTo(48);
 
         Configuration mdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -238,7 +236,7 @@
 
         mCarContext.onCarConfigurationChanged(mdpiConfig);
 
-        Drawable mdpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+        Drawable mdpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
         assertThat(mdpiDrawable.getIntrinsicHeight()).isEqualTo(64);
 
         Configuration hdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -246,12 +244,11 @@
 
         mCarContext.onCarConfigurationChanged(hdpiConfig);
 
-        Drawable hdpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+        Drawable hdpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
         assertThat(hdpiDrawable.getIntrinsicHeight()).isEqualTo(96);
     }
 
     @Test
-    @UiThreadTest
     // TODO(rampara): Investigate removing usage of deprecated updateConfiguration API
     @SuppressWarnings("deprecation")
     public void changingApplicationContextConfiguration_doesNotChangeTheCarContextConfiguration() {
@@ -260,7 +257,7 @@
 
         mCarContext.onCarConfigurationChanged(ldpiConfig);
 
-        Drawable ldpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+        Drawable ldpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
         assertThat(ldpiDrawable.getIntrinsicHeight()).isEqualTo(48);
 
         Configuration mdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -272,15 +269,15 @@
                 .updateConfiguration(mdpiConfig,
                         applicationContext.getResources().getDisplayMetrics());
 
-        Drawable carContextDrawable = mCarContext.getDrawable(R.drawable.banana);
+        Drawable carContextDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
         assertThat(carContextDrawable.getIntrinsicHeight()).isEqualTo(48);
 
-        Drawable applicationContextDrawable = applicationContext.getDrawable(R.drawable.banana);
+        Drawable applicationContextDrawable = TestUtils.getTestDrawable(applicationContext,
+                "banana");
         assertThat(applicationContextDrawable.getIntrinsicHeight()).isEqualTo(64);
     }
 
     @Test
-    @UiThreadTest
     // TODO(rampara): Investigate removing usage of deprecated updateConfiguration API
     @SuppressWarnings("deprecation")
     public void changingApplicationContextDisplayMetrics_doesNotChangeCarContextDisplayMetrics() {
@@ -289,7 +286,7 @@
 
         mCarContext.onCarConfigurationChanged(ldpiConfig);
 
-        Drawable ldpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+        Drawable ldpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
         assertThat(ldpiDrawable.getIntrinsicHeight()).isEqualTo(48);
 
         Configuration mdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -311,9 +308,8 @@
         applicationContext.getResources().updateConfiguration(mdpiConfig, newDisplayMetrics);
 
         assertThat(applicationContext.getResources().getConfiguration()).isEqualTo(mdpiConfig);
-        // TODO(rampara): Investigate why DisplayMetrics isn't updated
-//        assertThat(applicationContext.getResources().getDisplayMetrics()).isEqualTo(
-//                newDisplayMetrics);
+        assertThat(applicationContext.getResources().getDisplayMetrics()).isEqualTo(
+                newDisplayMetrics);
 
         assertThat(mCarContext.getResources().getConfiguration()).isNotEqualTo(mdpiConfig);
         assertThat(mCarContext.getResources().getDisplayMetrics()).isNotEqualTo(newDisplayMetrics);
@@ -352,60 +348,58 @@
         verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
     }
 
-    // TODO(rampara): Investigate how to mock final methods
-//    @Test
-//    public void
-//    getOnBackPressedDispatcher_withAListenerThatIsStarted_callsTheListenerAndDoesNotPop() {
-//        mCarContext.getCarService(ScreenManager.class).push(mScreen1);
-//        mCarContext.getCarService(ScreenManager.class).push(mScreen2);
-//
-//        OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
-//        when(callback.isEnabled()).thenReturn(true);
-//        mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
-//
-//        mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
-//        mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-//        verify(callback).handleOnBackPressed();
-//        verify(mMockScreen1, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
-//        verify(mMockScreen2, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
-//    }
-//
-//    @Test
-//    public void getOnBackPressedDispatcher_withAListenerThatIsNotStarted_popsAScreen() {
-//        mCarContext.getCarService(ScreenManager.class).push(mScreen1);
-//        mCarContext.getCarService(ScreenManager.class).push(mScreen2);
-//
-//        OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
-//        when(callback.isEnabled()).thenReturn(true);
-//        mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
-//
-//        mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
-//        mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-//        verify(callback, never()).handleOnBackPressed();
-//        verify(mMockScreen1, never()).dispatchLifecycleEvent(Lifecycle.Event.ON_DESTROY);
-//        verify(mMockScreen2).dispatchLifecycleEvent(Lifecycle.Event.ON_DESTROY);
-//    }
-//
-//    @Test
-//    public void getOnBackPressedDispatcher_callsDefaultListenerWheneverTheAddedOneIsNotSTARTED() {
-//        mCarContext.getCarService(ScreenManager.class).push(mScreen1);
-//        mCarContext.getCarService(ScreenManager.class).push(mScreen2);
-//
-//        OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
-//        when(callback.isEnabled()).thenReturn(true);
-//        mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
-//
-//        mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
-//        mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-//        verify(callback, never()).handleOnBackPressed();
-//        verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
-//
-//        mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
-//        mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-//        verify(callback).handleOnBackPressed();
-//    }
+    @Test
+    public void getOnBackPressedDispatcher_withListenerThatIsStarted_callsListenerAndDoesNotPop() {
+        mCarContext.getCarService(ScreenManager.class).push(mScreen1);
+        mCarContext.getCarService(ScreenManager.class).push(mScreen2);
+
+        OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
+        when(callback.isEnabled()).thenReturn(true);
+        mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
+
+        mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+        mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+        verify(callback).handleOnBackPressed();
+        verify(mMockScreen1, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
+        verify(mMockScreen2, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
+    }
+
+    @Test
+    public void getOnBackPressedDispatcher_withAListenerThatIsNotStarted_popsAScreen() {
+        mCarContext.getCarService(ScreenManager.class).push(mScreen1);
+        mCarContext.getCarService(ScreenManager.class).push(mScreen2);
+
+        OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
+        when(callback.isEnabled()).thenReturn(true);
+        mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
+
+        mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+        mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+        verify(callback, never()).handleOnBackPressed();
+        verify(mMockScreen1, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
+        verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
+    }
+
+    @Test
+    public void getOnBackPressedDispatcher_callsDefaultListenerWheneverTheAddedOneIsNotSTARTED() {
+        mCarContext.getCarService(ScreenManager.class).push(mScreen1);
+        mCarContext.getCarService(ScreenManager.class).push(mScreen2);
+
+        OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
+        when(callback.isEnabled()).thenReturn(true);
+        mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
+
+        mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+        mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+        verify(callback, never()).handleOnBackPressed();
+        verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
+
+        mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
+        mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+        verify(callback).handleOnBackPressed();
+    }
 }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarToastTest.java b/car/app/app/src/test/java/androidx/car/app/CarToastTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/CarToastTest.java
rename to car/app/app/src/test/java/androidx/car/app/CarToastTest.java
index 42f0e70..3ea0e15 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarToastTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarToastTest.java
@@ -22,23 +22,21 @@
 
 import androidx.car.app.testing.TestAppManager;
 import androidx.car.app.testing.TestCarContext;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link CarToast}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public final class CarToastTest {
     private TestCarContext mCarContext;
 
     @Before
-    @UiThreadTest
     public void setUp() {
         mCarContext = TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
         mCarContext.reset();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/HandshakeInfoTest.java b/car/app/app/src/test/java/androidx/car/app/HandshakeInfoTest.java
similarity index 77%
rename from car/app/app/src/androidTest/java/androidx/car/app/HandshakeInfoTest.java
rename to car/app/app/src/test/java/androidx/car/app/HandshakeInfoTest.java
index 40b39c8..a367e7a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/HandshakeInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/HandshakeInfoTest.java
@@ -18,20 +18,21 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public final class HandshakeInfoTest {
 
     @Test
     public void construct_handshakeInfo() {
         String hostPackageName = "com.google.host";
-        HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName);
+        int hostApiLevel = 123;
+        HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
         assertThat(handshakeInfo.getHostPackageName()).isEqualTo(hostPackageName);
+        assertThat(handshakeInfo.getHostCarAppApiLevel()).isEqualTo(hostApiLevel);
     }
 }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/HostDispatcherTest.java b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/HostDispatcherTest.java
rename to car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
index 3e5bc5c..1366265 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/HostDispatcherTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
@@ -29,19 +29,19 @@
 import androidx.annotation.Nullable;
 import androidx.car.app.navigation.INavigationHost;
 import androidx.car.app.serialization.Bundleable;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-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 org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link HostDispatcher}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class HostDispatcherTest {
 
     @Mock
@@ -55,7 +55,6 @@
     private HostDispatcher mHostDispatcher = new HostDispatcher();
 
     @Before
-    @UiThreadTest
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
 
@@ -149,7 +148,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void getHost_afterResetting_getsFromCarHost() throws RemoteException {
         assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isEqualTo(mAppHost);
 
@@ -211,7 +209,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void getHost_afterReset_throwsHostException() {
         mHostDispatcher.resetHosts();
 
@@ -219,7 +216,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void getHost_notBound_throwsHostException() {
         mHostDispatcher = new HostDispatcher();
 
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/ScreenManagerTest.java b/car/app/app/src/test/java/androidx/car/app/ScreenManagerTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/ScreenManagerTest.java
rename to car/app/app/src/test/java/androidx/car/app/ScreenManagerTest.java
index 383164a..aceb9cb 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/ScreenManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/ScreenManagerTest.java
@@ -34,10 +34,7 @@
 import androidx.car.app.testing.TestLifecycleOwner;
 import androidx.lifecycle.Lifecycle.Event;
 import androidx.lifecycle.Lifecycle.State;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -45,10 +42,12 @@
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link ScreenManager}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public final class ScreenManagerTest {
 
     private TestScreen mScreen1;
@@ -61,7 +60,7 @@
     @Mock
     private Screen mMockScreen3;
     @Mock
-    private OnScreenResultCallback mOnScreenResultCallback;
+    private OnScreenResultListener mOnScreenResultListener;
 
     @Mock
     private AppManager mMockAppManager;
@@ -71,7 +70,6 @@
     private ScreenManager mScreenManager;
 
     @Before
-    @UiThreadTest
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
@@ -131,7 +129,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void push_stackHadScreen_addsToStack_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -158,7 +155,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void push_screenAlreadyInStack_movesToTop_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -194,7 +190,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void push_screenAlreadyTopOfStack_noFurtherLifecycleCalls() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -223,14 +218,13 @@
     }
 
     @Test
-    @UiThreadTest
     public void pushForResult_addsToStack_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager,
-                mOnScreenResultCallback);
+                mOnScreenResultListener);
 
         mScreenManager.push(mScreen1);
-        mScreenManager.pushForResult(mScreen2, mOnScreenResultCallback);
+        mScreenManager.pushForResult(mScreen2, mOnScreenResultListener);
 
         inOrder.verify(mMockScreen1).dispatchLifecycleEvent(Event.ON_CREATE);
         inOrder.verify(mMockAppManager).invalidate();
@@ -251,32 +245,30 @@
     }
 
     @Test
-    @UiThreadTest
     public void pushForResult_screenSetsResult_firstScreenGetsCalled() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
 
         mScreenManager.push(mScreen1);
-        mScreenManager.pushForResult(mScreen2, mOnScreenResultCallback);
+        mScreenManager.pushForResult(mScreen2, mOnScreenResultListener);
 
         String result = "done";
         mScreen2.setResult(result);
 
-        verify(mOnScreenResultCallback, never()).onScreenResult(any());
+        verify(mOnScreenResultListener, never()).onScreenResult(any());
 
         mScreenManager.remove(mScreen2);
 
-        verify(mOnScreenResultCallback).onScreenResult(result);
+        verify(mOnScreenResultListener).onScreenResult(result);
     }
 
     @Test
-    @UiThreadTest
     public void pushForResult_screenSetsResult_firstScreenGetsCalled_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager,
-                mOnScreenResultCallback);
+                mOnScreenResultListener);
 
         mScreenManager.push(mScreen1);
-        mScreenManager.pushForResult(mScreen2, mOnScreenResultCallback);
+        mScreenManager.pushForResult(mScreen2, mOnScreenResultListener);
 
         String result = "foo";
         mScreen2.setResult(result);
@@ -305,7 +297,7 @@
         inOrder.verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_PAUSE);
         inOrder.verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_STOP);
 
-        inOrder.verify(mOnScreenResultCallback).onScreenResult(result);
+        inOrder.verify(mOnScreenResultListener).onScreenResult(result);
         inOrder.verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
         inOrder.verify(mMockScreen1).dispatchLifecycleEvent(Event.ON_RESUME);
 
@@ -347,7 +339,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void pop_removes_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -445,7 +436,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popTo_pops_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -486,7 +476,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popTo_markerScreenNotInStack_popsToRoot_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -537,7 +526,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popTo_multipleToPop_pops_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -591,7 +579,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popTo_notARootTarget_popsExpectedScreens_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -647,7 +634,7 @@
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockAppManager);
 
-        mScreenManager.popTo(Screen.ROOT);
+        mScreenManager.popToRoot();
 
         inOrder.verifyNoMoreInteractions();
 
@@ -661,7 +648,7 @@
 
         mScreenManager.push(mScreen1);
 
-        mScreenManager.popTo(Screen.ROOT);
+        mScreenManager.popToRoot();
 
         // Pushing screen1
         inOrder.verify(mMockScreen1).dispatchLifecycleEvent(Event.ON_CREATE);
@@ -675,7 +662,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popToRoot_pops_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -683,7 +669,7 @@
         mScreenManager.push(mScreen1);
         mScreenManager.push(mScreen2);
 
-        mScreenManager.popTo(Screen.ROOT);
+        mScreenManager.popToRoot();
 
         // Pushing screen1
         inOrder.verify(mMockScreen1).dispatchLifecycleEvent(Event.ON_CREATE);
@@ -713,7 +699,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popToRoot_multipleToPop_pops_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -722,7 +707,7 @@
         mScreenManager.push(mScreen2);
         mScreenManager.push(mScreen3);
 
-        mScreenManager.popTo(Screen.ROOT);
+        mScreenManager.popToRoot();
 
         // Pushing screen1
         inOrder.verify(mMockScreen1).dispatchLifecycleEvent(Event.ON_CREATE);
@@ -764,24 +749,23 @@
     }
 
     @Test
-    @UiThreadTest
     public void popTo_multipleToPop_withResult_pops_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder =
                 inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager,
-                        mOnScreenResultCallback);
+                        mOnScreenResultListener);
 
         mScreenManager.push(mScreen1);
 
-        mScreenManager.pushForResult(mScreen2, mOnScreenResultCallback);
+        mScreenManager.pushForResult(mScreen2, mOnScreenResultListener);
         Object result1 = "foo";
         mScreen2.setResult(result1);
 
-        mScreenManager.pushForResult(mScreen3, mOnScreenResultCallback);
+        mScreenManager.pushForResult(mScreen3, mOnScreenResultListener);
         Object result2 = "bar";
         mScreen3.setResult(result2);
 
-        mScreenManager.popTo(Screen.ROOT);
+        mScreenManager.popToRoot();
 
         // Pushing screen1
         inOrder.verify(mMockScreen1).dispatchLifecycleEvent(Event.ON_CREATE);
@@ -811,10 +795,10 @@
 
         inOrder.verify(mMockScreen3).dispatchLifecycleEvent(Event.ON_PAUSE);
         inOrder.verify(mMockScreen3).dispatchLifecycleEvent(Event.ON_STOP);
-        inOrder.verify(mOnScreenResultCallback).onScreenResult(result2);
+        inOrder.verify(mOnScreenResultListener).onScreenResult(result2);
         inOrder.verify(mMockScreen3).dispatchLifecycleEvent(Event.ON_DESTROY);
 
-        inOrder.verify(mOnScreenResultCallback).onScreenResult(result1);
+        inOrder.verify(mOnScreenResultListener).onScreenResult(result1);
         inOrder.verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
 
         inOrder.verify(mMockScreen1).dispatchLifecycleEvent(Event.ON_RESUME);
@@ -858,7 +842,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void remove_wasTop_removes_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -897,7 +880,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void remove_wasNotTop_removes_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -930,7 +912,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void remove_notInStack_noop() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -960,7 +941,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void getTopTemplate_returnsTemplateFromTopOfStack() {
         Template template =
                 PlaceListMapTemplate.builder()
@@ -972,8 +952,8 @@
                         .setTitle("Title2")
                         .setItemList(ItemList.builder().build())
                         .build();
-        when(mMockScreen1.getTemplate()).thenReturn(template);
-        when(mMockScreen2.getTemplate()).thenReturn(template2);
+        when(mMockScreen1.onGetTemplate()).thenReturn(template);
+        when(mMockScreen2.onGetTemplate()).thenReturn(template2);
 
         mScreenManager.push(mScreen1);
         mScreenManager.push(mScreen2);
@@ -993,7 +973,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onStart_expectedLifecycleChange() {
         mScreenManager.push(mScreen1);
         reset(mMockScreen1);
@@ -1009,7 +988,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onResume_expectedLifecycleChange() {
         mScreenManager.push(mScreen1);
         reset(mMockScreen1);
@@ -1025,7 +1003,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onPause_expectedLifecycleChange() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         mScreenManager.push(mScreen1);
@@ -1042,7 +1019,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onStop_expectedLifecycleChange() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         mScreenManager.push(mScreen1);
@@ -1067,7 +1043,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onDestroy_screenStopped_onlyDestroys() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         mScreenManager.push(mScreen1);
@@ -1083,7 +1058,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onDestroy_screenPaused_stopsAndDestroys() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         mScreenManager.push(mScreen1);
@@ -1100,7 +1074,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onDestroy_screenResumed_pausesStopsAndDestroys() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         mScreenManager.push(mScreen1);
@@ -1117,7 +1090,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void dispatchAppLifecycleEvent_onDestroy_pausesStopsAndDestroysTop_destroysOthers() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         mScreenManager.push(mScreen1);
@@ -1140,7 +1112,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void pop_screenReuseLastTemplateId() {
         Template template =
                 PlaceListMapTemplate.builder()
@@ -1152,9 +1123,9 @@
                         .setTitle("Title")
                         .setItemList(ItemList.builder().build())
                         .build();
-        when(mMockScreen1.getTemplate()).thenReturn(template);
-        when(mMockScreen2.getTemplate()).thenReturn(template);
-        when(mMockScreen3.getTemplate()).thenReturn(template);
+        when(mMockScreen1.onGetTemplate()).thenReturn(template);
+        when(mMockScreen2.onGetTemplate()).thenReturn(template);
+        when(mMockScreen3.onGetTemplate()).thenReturn(template);
 
         mScreenManager.push(mScreen1);
         TemplateWrapper wrapper1 = mScreenManager.getTopTemplate();
@@ -1167,8 +1138,8 @@
         mScreenManager.push(mScreen3);
         mScreenManager.pop();
 
-        when(mMockScreen1.getTemplate()).thenReturn(template2);
-        when(mMockScreen2.getTemplate()).thenReturn(template2);
+        when(mMockScreen1.onGetTemplate()).thenReturn(template2);
+        when(mMockScreen2.onGetTemplate()).thenReturn(template2);
 
         // Popping should reuse the last template's id of screen2.
         wrapper2 = mScreenManager.getTopTemplate();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/ScreenTest.java b/car/app/app/src/test/java/androidx/car/app/ScreenTest.java
similarity index 86%
rename from car/app/app/src/androidTest/java/androidx/car/app/ScreenTest.java
rename to car/app/app/src/test/java/androidx/car/app/ScreenTest.java
index 7b3902e..807337c 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/ScreenTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/ScreenTest.java
@@ -31,30 +31,28 @@
 import androidx.car.app.testing.TestScreenManager;
 import androidx.lifecycle.Lifecycle.Event;
 import androidx.lifecycle.Lifecycle.State;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-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 org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Screen}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public final class ScreenTest {
     private TestCarContext mCarContext;
 
     @Mock
-    OnScreenResultCallback mMockOnScreenResultCallback;
+    OnScreenResultListener mMockOnScreenResultListener;
 
     private Screen mScreen;
 
     @Before
-    @UiThreadTest
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mCarContext =
@@ -68,7 +66,7 @@
                         new Screen(mCarContext) {
                             @Override
                             @NonNull
-                            public Template getTemplate() {
+                            public Template onGetTemplate() {
                                 return new Template() {
                                 };
                             }
@@ -77,7 +75,7 @@
         mScreen = new Screen(mCarContext) {
             @Override
             @NonNull
-            public Template getTemplate() {
+            public Template onGetTemplate() {
                 return PlaceListMapTemplate.builder().setItemList(
                         ItemList.builder().build()).build();
             }
@@ -87,7 +85,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void finish_removesSelf() {
         mScreen.finish();
         assertThat(mCarContext.getCarService(TestScreenManager.class).getScreensRemoved())
@@ -95,14 +92,12 @@
     }
 
     @Test
-    @UiThreadTest
     public void onCreate_expectedLifecycleChange() {
         mScreen.dispatchLifecycleEvent(Event.ON_CREATE);
         assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.CREATED);
     }
 
     @Test
-    @UiThreadTest
     public void onStart_expectedLifecycleChange() {
         mScreen.dispatchLifecycleEvent(Event.ON_START);
         assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.STARTED);
@@ -115,43 +110,38 @@
     }
 
     @Test
-    @UiThreadTest
     public void onPause_expectedLifecycleChange() {
         mScreen.dispatchLifecycleEvent(Event.ON_PAUSE);
         assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.STARTED);
     }
 
     @Test
-    @UiThreadTest
     public void onStop_expectedLifecycleChange() {
         mScreen.dispatchLifecycleEvent(Event.ON_STOP);
         assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.CREATED);
     }
 
     @Test
-    @UiThreadTest
     public void onDestroy_expectedLifecycleChange() {
         mScreen.dispatchLifecycleEvent(Event.ON_DESTROY);
         assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.DESTROYED);
     }
 
     @Test
-    @UiThreadTest
     public void setResult_callsThemockOnScreenResultCallback() {
-        mScreen.setOnResultCallback(mMockOnScreenResultCallback);
+        mScreen.setOnScreenResultListener(mMockOnScreenResultListener);
 
         String foo = "yo";
         mScreen.setResult(foo);
 
-        verify(mMockOnScreenResultCallback, never()).onScreenResult(any());
+        verify(mMockOnScreenResultListener, never()).onScreenResult(any());
 
         mScreen.dispatchLifecycleEvent(Event.ON_DESTROY);
 
-        verify(mMockOnScreenResultCallback).onScreenResult(foo);
+        verify(mMockOnScreenResultListener).onScreenResult(foo);
     }
 
     @Test
-    @UiThreadTest
     public void finish_screenIsDestroyed() {
         mScreen.finish();
         assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.DESTROYED);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestData.java b/car/app/app/src/test/java/androidx/car/app/TestData.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/TestData.java
rename to car/app/app/src/test/java/androidx/car/app/TestData.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestScreen.java b/car/app/app/src/test/java/androidx/car/app/TestScreen.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/TestScreen.java
rename to car/app/app/src/test/java/androidx/car/app/TestScreen.java
index 77f6720..e0d2cbe 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/TestScreen.java
+++ b/car/app/app/src/test/java/androidx/car/app/TestScreen.java
@@ -31,8 +31,8 @@
 
     @NonNull
     @Override
-    public Template getTemplate() {
-        return mScreenForMocking.getTemplate();
+    public Template onGetTemplate() {
+        return mScreenForMocking.onGetTemplate();
     }
 
     @Override
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java b/car/app/app/src/test/java/androidx/car/app/TestUtils.java
similarity index 89%
rename from car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
rename to car/app/app/src/test/java/androidx/car/app/TestUtils.java
index 5378ae2..7c6f6cc 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
+++ b/car/app/app/src/test/java/androidx/car/app/TestUtils.java
@@ -27,8 +27,10 @@
 
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
 import android.text.SpannableString;
 
+import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -42,6 +44,7 @@
 import androidx.car.app.model.Pane;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.SectionedItemList;
+import androidx.core.graphics.drawable.IconCompat;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -59,6 +62,20 @@
     private TestUtils() {
     }
 
+    public static Drawable getTestDrawable(Context context, String drawable) {
+        return context.getDrawable(getTestDrawableResId(context, drawable));
+    }
+
+    public static CarIcon getTestCarIcon(Context context, String drawable) {
+        return CarIcon.of(IconCompat.createWithResource(context,
+                TestUtils.getTestDrawableResId(context, drawable)));
+    }
+
+    @DrawableRes
+    public static int getTestDrawableResId(Context context, String drawable) {
+        return context.getResources().getIdentifier(drawable, "drawable", context.getPackageName());
+    }
+
     /**
      * Returns a {@link DateTimeWithZone} instance from a date string and a time zone id.
      *
@@ -113,7 +130,7 @@
         }
 
         if (isSelectable) {
-            builder.setSelectable(index -> {
+            builder.setOnSelectedListener(index -> {
             });
         }
 
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java b/car/app/app/src/test/java/androidx/car/app/model/ActionStripTest.java
similarity index 83%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ActionStripTest.java
index e6efb98..778db2d 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ActionStripTest.java
@@ -20,15 +20,14 @@
 
 import static org.junit.Assert.assertThrows;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link ActionStrip}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ActionStripTest {
     @Test
     public void createEmpty_throws() {
@@ -36,6 +35,20 @@
     }
 
     @Test
+    public void defaultBackgroundColor_doesNotThrow() {
+        Action action = Action.builder().setTitle("Test").setBackgroundColor(
+                CarColor.DEFAULT).build();
+    }
+
+    @Test
+    public void backgroundColor_throws() {
+        Action action1 = Action.builder().setTitle("Test").setBackgroundColor(
+                CarColor.BLUE).build();
+        assertThrows(IllegalArgumentException.class,
+                () -> ActionStrip.builder().addAction(action1));
+    }
+
+    @Test
     public void addDuplicatedTypes_throws() {
         Action action1 = Action.BACK;
         Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
@@ -61,20 +74,6 @@
     }
 
     @Test
-    public void clearActions() {
-        Action action1 = Action.BACK;
-        Action action2 = Action.APP_ICON;
-        ActionStrip list =
-                ActionStrip.builder()
-                        .addAction(action1)
-                        .addAction(action2)
-                        .clearActions()
-                        .addAction(action2)
-                        .build();
-        assertThat(list.getActions()).hasSize(1);
-    }
-
-    @Test
     public void getActionOfType() {
         Action action1 = Action.BACK;
         Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java b/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
similarity index 81%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
index 8b48fe2..1535bf9 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
@@ -23,16 +23,14 @@
 import static org.mockito.Mockito.verify;
 
 import android.content.ContentResolver;
+import android.content.Context;
 import android.net.Uri;
 
 import androidx.car.app.IOnDoneCallback;
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.test.R;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.TestUtils;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -40,10 +38,12 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Action}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ActionTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
@@ -79,23 +79,20 @@
         assertThrows(
                 IllegalArgumentException.class,
                 () -> Action.builder()
-                                .setTitle("foo")
-                                .setOnClickListener(onClickListener)
-                                .setBackgroundColor(CarColor.createCustom(0xdead, 0xbeef))
-                                .build());
+                        .setTitle("foo")
+                        .setOnClickListener(onClickListener)
+                        .setBackgroundColor(CarColor.createCustom(0xdead, 0xbeef))
+                        .build());
     }
 
     @Test
     public void create_noTitleDefault() {
         OnClickListener onClickListener = mock(OnClickListener.class);
         Action action = Action.builder()
-                        .setIcon(
-                                CarIcon.of(
-                                        IconCompat.createWithResource(
-                                                ApplicationProvider.getApplicationContext(),
-                                                R.drawable.ic_test_1)))
-                        .setOnClickListener(onClickListener)
-                        .build();
+                .setIcon(TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                        "ic_test_1"))
+                .setOnClickListener(onClickListener)
+                .build();
         assertThat(action.getTitle()).isNull();
     }
 
@@ -116,19 +113,18 @@
     }
 
     @Test
-    @UiThreadTest
     public void createInstance() {
         OnClickListener onClickListener = mock(OnClickListener.class);
-        IconCompat icon =
-                IconCompat.createWithResource(
-                        ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+        Context context = ApplicationProvider.getApplicationContext();
+        IconCompat icon = IconCompat.createWithResource(
+                context, TestUtils.getTestDrawableResId(context, "ic_test_1"));
         String title = "foo";
         Action action = Action.builder()
-                        .setTitle(title)
-                        .setIcon(CarIcon.of(icon))
-                        .setBackgroundColor(CarColor.BLUE)
-                        .setOnClickListener(onClickListener)
-                        .build();
+                .setTitle(title)
+                .setIcon(CarIcon.of(icon))
+                .setBackgroundColor(CarColor.BLUE)
+                .setOnClickListener(onClickListener)
+                .build();
         assertThat(icon).isEqualTo(action.getIcon().getIcon());
         assertThat(CarText.create(title)).isEqualTo(action.getTitle());
         assertThat(CarColor.BLUE).isEqualTo(action.getBackgroundColor());
@@ -199,9 +195,9 @@
         CarIcon icon2 = CarIcon.APP_ICON;
 
         Action action1 = Action.builder().setOnClickListener(() -> {
-                }).setTitle(title).setIcon(icon1).build();
+        }).setTitle(title).setIcon(icon1).build();
         Action action2 = Action.builder().setOnClickListener(() -> {
-                }).setTitle(title).setIcon(icon2).build();
+        }).setTitle(title).setIcon(icon2).build();
 
         assertThat(action2).isNotEqualTo(action1);
     }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/CarIconSpanTest.java
similarity index 84%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/CarIconSpanTest.java
index 2439237..0237996 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/CarIconSpanTest.java
@@ -21,29 +21,30 @@
 import static org.junit.Assert.assertThrows;
 
 import android.content.ContentResolver;
+import android.content.Context;
 import android.net.Uri;
 
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link CarIconSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class CarIconSpanTest {
     private IconCompat mIcon;
 
     @Before
     public void setup() {
-        mIcon =
-                IconCompat.createWithResource(
-                        ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+        Context context = ApplicationProvider.getApplicationContext();
+        mIcon = IconCompat.createWithResource(
+                context, TestUtils.getTestDrawableResId(context, "ic_test_1"));
     }
 
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java b/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
similarity index 87%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
index 1152c7e..e6a33ab 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
@@ -28,32 +28,33 @@
 import static org.junit.Assert.assertThrows;
 
 import android.content.ContentResolver;
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.net.Uri;
 
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.io.File;
 
 /** Tests for {@link CarIcon}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class CarIconTest {
     private IconCompat mIcon;
 
     @Before
     public void setup() {
-        mIcon =
-                IconCompat.createWithResource(
-                        ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+        Context context = ApplicationProvider.getApplicationContext();
+        mIcon = IconCompat.createWithResource(
+                context, TestUtils.getTestDrawableResId(context, "ic_test_1"));
     }
 
     @Test
@@ -142,11 +143,10 @@
     public void equals() {
         assertThat(BACK.equals(BACK)).isTrue();
         CarIcon carIcon = CarIcon.of(mIcon);
+        Context context = ApplicationProvider.getApplicationContext();
 
-        assertThat(
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1)))
+        assertThat(CarIcon.of(IconCompat.createWithResource(
+                context, TestUtils.getTestDrawableResId(context, "ic_test_1"))))
                 .isEqualTo(carIcon);
     }
 
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java b/car/app/app/src/test/java/androidx/car/app/model/DateTimeWithZoneTest.java
similarity index 85%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DateTimeWithZoneTest.java
index 1844d51..43bdbf0 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DateTimeWithZoneTest.java
@@ -24,12 +24,10 @@
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.time.Duration;
 import java.time.ZonedDateTime;
@@ -38,8 +36,8 @@
 import java.util.TimeZone;
 
 /** Tests for {@link DateTimeWithZone}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class DateTimeWithZoneTest {
     @Test
     @SuppressWarnings("JdkObsolete")
@@ -73,36 +71,29 @@
         // Negative time.
         assertThrows(
                 IllegalArgumentException.class,
-                () -> {
-                    DateTimeWithZone.create(-1, (int) timeZoneOffsetSeconds, zoneShortName);
-                });
+                () -> DateTimeWithZone.create(-1, (int) timeZoneOffsetSeconds, zoneShortName));
 
         // Offset out of range.
         assertThrows(
                 IllegalArgumentException.class,
-                () -> {
-                    DateTimeWithZone.create(timeSinceEpochMillis, 18 * 60 * 60 + 1, zoneShortName);
-                });
+                () -> DateTimeWithZone.create(timeSinceEpochMillis, 18 * 60 * 60 + 1,
+                        zoneShortName));
         assertThrows(
                 IllegalArgumentException.class,
-                () -> {
-                    DateTimeWithZone.create(timeSinceEpochMillis, -18 * 60 * 60 - 1, zoneShortName);
-                });
+                () -> DateTimeWithZone.create(timeSinceEpochMillis, -18 * 60 * 60 - 1,
+                        zoneShortName));
 
         // Null short name.
         assertThrows(
                 NullPointerException.class,
-                () -> {
-                    DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
-                            null);
-                });
+                () -> DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                        null));
 
         // Empty short name.
         assertThrows(
                 IllegalArgumentException.class,
-                () -> {
-                    DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds, "");
-                });
+                () -> DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+                        ""));
     }
 
     @Test
@@ -131,20 +122,15 @@
         // Negative time.
         assertThrows(
                 IllegalArgumentException.class,
-                () -> {
-                    DateTimeWithZone.create(-1, timeZone);
-                });
+                () -> DateTimeWithZone.create(-1, timeZone));
 
         // Null time zone.
         assertThrows(
                 NullPointerException.class,
-                () -> {
-                    DateTimeWithZone.create(123, null);
-                });
+                () -> DateTimeWithZone.create(123, null));
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
     public void create_withZonedDateTime() {
         ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
         DateTimeWithZone dateTimeWithZone = DateTimeWithZone.create(zonedDateTime);
@@ -153,7 +139,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
     public void create_withZonedDateTime_argumentChecks() {
         // Null date time.
         assertThrows(
@@ -164,7 +149,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
     public void equals() {
         TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
         long timeSinceEpochMillis = System.currentTimeMillis();
@@ -183,7 +167,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
     public void notEquals_differentTimeSinceEpoch() {
         TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
         long timeSinceEpochMillis = System.currentTimeMillis();
@@ -203,7 +186,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
     public void notEquals_differentTimeZoneOffsetSeconds() {
         TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
         long timeSinceEpochMillis = System.currentTimeMillis();
@@ -223,7 +205,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
     public void notEquals_differentTimeZone() {
         TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
         long timeSinceEpochMillis = System.currentTimeMillis();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/DistanceSpanTest.java
similarity index 90%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DistanceSpanTest.java
index a05bed7..704770b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DistanceSpanTest.java
@@ -18,15 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link DistanceSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class DistanceSpanTest {
     private final Distance mDistance =
             Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java b/car/app/app/src/test/java/androidx/car/app/model/DistanceTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DistanceTest.java
index 91c7028..4ebe6fb 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DistanceTest.java
@@ -24,15 +24,14 @@
 
 import static org.junit.Assert.assertThrows;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Distance}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class DistanceTest {
 
     private static final double DISPLAY_DISTANCE = 1.2d;
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/DurationSpanTest.java
similarity index 88%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DurationSpanTest.java
index e43f2e1..cce7162 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DurationSpanTest.java
@@ -18,15 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link DurationSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class DurationSpanTest {
     @Test
     public void constructor() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
similarity index 91%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
index 4f320f0..a3b92a1 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
@@ -23,15 +23,14 @@
 
 import static org.junit.Assert.assertThrows;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link CarIconSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ForegroundCarColorSpanTest {
     @Test
     public void constructor() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java b/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
similarity index 77%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
index 5fc726f0..3c767ed 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
@@ -27,26 +27,24 @@
 
 import android.os.RemoteException;
 
-import androidx.car.app.host.OnDoneCallback;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.car.app.OnDoneCallback;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link GridItem}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class GridItemTest {
 
     @Test
     public void create_defaultValues() {
-        GridItem gridItem = GridItem.builder().setImage(BACK).build();
+        GridItem gridItem = GridItem.builder().setTitle("Title").setImage(BACK).build();
 
         assertThat(BACK).isEqualTo(gridItem.getImage());
         assertThat(gridItem.getImageType()).isEqualTo(GridItem.IMAGE_TYPE_LARGE);
-        assertThat(gridItem.getTitle()).isNull();
         assertThat(gridItem.getText()).isNull();
     }
 
@@ -59,6 +57,17 @@
     }
 
     @Test
+    public void title_throwsIfNotSet() {
+        // Not set
+        assertThrows(IllegalStateException.class, () -> GridItem.builder().setImage(BACK).build());
+
+        // Not set
+        assertThrows(
+                IllegalArgumentException.class, () -> GridItem.builder().setTitle("").setImage(
+                        BACK).build());
+    }
+
+    @Test
     public void text_charSequence() {
         String text = "foo";
         GridItem gridItem = GridItem.builder().setTitle("title").setText(text).setImage(
@@ -110,9 +119,10 @@
 
     @Test
     public void notEquals_differentImage() {
-        GridItem gridItem = GridItem.builder().setImage(BACK).build();
+        GridItem gridItem = GridItem.builder().setTitle("Title").setImage(BACK).build();
 
-        assertThat(GridItem.builder().setImage(ALERT).build()).isNotEqualTo(gridItem);
+        assertThat(GridItem.builder().setImage(ALERT).setTitle("Title").build()).isNotEqualTo(
+                gridItem);
     }
 
     @Test
@@ -121,18 +131,20 @@
         }).setChecked(true).build();
         Toggle toggle2 = Toggle.builder(isChecked -> {
         }).setChecked(false).build();
-        GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle1).build();
+        GridItem gridItem = GridItem.builder().setTitle("Title").setImage(BACK).setToggle(
+                toggle1).build();
 
-        assertThat(GridItem.builder().setImage(BACK).setToggle(toggle2).build()).isNotEqualTo(
+        assertThat(GridItem.builder().setImage(BACK).setTitle("Title").setToggle(
+                toggle2).build()).isNotEqualTo(
                 gridItem);
     }
 
     @Test
-    @UiThreadTest
     public void clickListener() throws RemoteException {
         OnClickListener onClickListener = mock(OnClickListener.class);
         GridItem gridItem =
-                GridItem.builder().setImage(BACK).setOnClickListener(onClickListener).build();
+                GridItem.builder().setTitle("Title").setImage(BACK).setOnClickListener(
+                        onClickListener).build();
         OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
         gridItem.getOnClickListener().onClick(onDoneCallback);
         verify(onClickListener).onClick();
@@ -143,7 +155,8 @@
     public void setToggle() {
         Toggle toggle = Toggle.builder(isChecked -> {
         }).build();
-        GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle).build();
+        GridItem gridItem =
+                GridItem.builder().setTitle("Title").setImage(BACK).setToggle(toggle).build();
         assertThat(toggle).isEqualTo(gridItem.getToggle());
     }
 
diff --git a/car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
new file mode 100644
index 0000000..6078ee3
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.model;
+
+import static androidx.car.app.model.CarIcon.BACK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.TestUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/** Tests for {@link GridTemplate}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class GridTemplateTest {
+    @Test
+    public void createInstance_emptyList_notLoading_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> GridTemplate.builder().setTitle("Title").build());
+
+        // Positive case
+        GridTemplate.builder().setTitle("Title").setLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_isLoading_hasList_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setLoading(true)
+                                .setSingleList(TestUtils.getGridItemList(2))
+                                .build());
+    }
+
+    @Test
+    public void createInstance_noHeaderTitleOrAction_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> GridTemplate.builder().setSingleList(TestUtils.getGridItemList(2)).build());
+
+        // Positive cases.
+        GridTemplate.builder().setTitle("Title").setSingleList(
+                TestUtils.getGridItemList(2)).build();
+        GridTemplate.builder()
+                .setHeaderAction(Action.BACK)
+                .setSingleList(TestUtils.getGridItemList(2))
+                .build();
+    }
+
+    @Test
+    public void createInstance_setSingleList() {
+        ItemList list = TestUtils.getGridItemList(2);
+        GridTemplate template = GridTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+        assertThat(template.getSingleList()).isEqualTo(list);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        GridTemplate.builder()
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(TestUtils.getGridItemList(2))
+                        .setHeaderAction(Action.BACK)
+                        .build();
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void createInstance_setActionStrip() {
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(TestUtils.getGridItemList(2))
+                        .setTitle("Title")
+                        .setActionStrip(actionStrip)
+                        .build();
+        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+    }
+
+    @Test
+    public void createInstance_setBackground() {
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setTitle("Title")
+                        .setLoading(true)
+                        .setBackgroundImage(BACK)
+                        .build();
+        assertThat(template.getBackgroundImage()).isEqualTo(BACK);
+    }
+
+    @Test
+    public void resetList_clearsSingleList() {
+        GridTemplate.Builder builder =
+                GridTemplate.builder()
+                        .setSingleList(TestUtils.getGridItemList(2))
+                        .setHeaderAction(Action.BACK);
+
+        assertThrows(IllegalStateException.class, () -> builder.clearAllLists().build());
+    }
+
+    @Test
+    public void equals() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(itemList)
+                        .setHeaderAction(Action.BACK)
+                        .setActionStrip(actionStrip)
+                        .setTitle(title)
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        GridTemplate.builder()
+                                .setSingleList(itemList)
+                                .setHeaderAction(Action.BACK)
+                                .setActionStrip(actionStrip)
+                                .setTitle(title)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        ItemList itemList = ItemList.builder().build();
+
+        GridTemplate template =
+                GridTemplate.builder().setTitle("Title 1").setSingleList(itemList).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        GridTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(
+                                                GridItem.builder().setTitle("Title 2").setImage(
+                                                        BACK).build()).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        ItemList itemList = ItemList.builder().build();
+
+        GridTemplate template =
+                GridTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        GridTemplate.builder()
+                                .setSingleList(itemList)
+                                .setHeaderAction(Action.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+
+        GridTemplate template = GridTemplate.builder().setSingleList(itemList).setTitle(
+                title).build();
+
+        assertThat(template)
+                .isNotEqualTo(GridTemplate.builder().setSingleList(itemList).setTitle(
+                        "foo").build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+
+        GridTemplate template =
+                GridTemplate.builder()
+                        .setSingleList(itemList)
+                        .setTitle(title)
+                        .setActionStrip(ActionStrip.builder().addAction(Action.BACK).build())
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        GridTemplate.builder()
+                                .setSingleList(itemList)
+                                .setTitle(title)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build());
+    }
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java b/car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
new file mode 100644
index 0000000..e306d1b
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.model;
+
+import static androidx.car.app.model.CarIcon.BACK;
+import static androidx.car.app.model.ItemList.builder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.WrappedRuntimeException;
+import androidx.car.app.model.ItemList.OnItemVisibilityChangedListener;
+import androidx.car.app.model.ItemList.OnSelectedListener;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.Collections;
+
+/** Tests for {@link ItemListTest}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class ItemListTest {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private IOnDoneCallback.Stub mMockOnDoneCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void createEmpty() {
+        ItemList list = builder().build();
+        assertThat(list.getItems()).isEqualTo(Collections.emptyList());
+    }
+
+    @Test
+    public void createRows() {
+        Row row1 = Row.builder().setTitle("Row1").build();
+        Row row2 = Row.builder().setTitle("Row2").build();
+        ItemList list = builder().addItem(row1).addItem(row2).build();
+
+        assertThat(list.getItems()).hasSize(2);
+        assertThat(list.getItems().get(0)).isEqualTo(row1);
+        assertThat(list.getItems().get(1)).isEqualTo(row2);
+    }
+
+    @Test
+    public void createGridItems() {
+        GridItem gridItem1 = GridItem.builder().setTitle("title 1").setImage(BACK).build();
+        GridItem gridItem2 = GridItem.builder().setTitle("title 2").setImage(BACK).build();
+        ItemList list = builder().addItem(gridItem1).addItem(gridItem2).build();
+
+        assertThat(list.getItems()).containsExactly(gridItem1, gridItem2).inOrder();
+    }
+
+    @Test
+    public void setSelectedable_emptyList_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> builder().setOnSelectedListener(selectedIndex -> {
+                }).build());
+    }
+
+    @Test
+    public void setSelectedIndex_greaterThanListSize_throws() {
+        Row row1 = Row.builder().setTitle("Row1").build();
+        assertThrows(
+                IllegalStateException.class,
+                () -> builder()
+                        .addItem(row1)
+                        .setOnSelectedListener(selectedIndex -> {
+                        })
+                        .setSelectedIndex(2)
+                        .build());
+    }
+
+    @Test
+    public void setSelectable() throws RemoteException {
+        OnSelectedListener mockListener = mock(OnSelectedListener.class);
+        ItemList itemList =
+                builder()
+                        .addItem(Row.builder().setTitle("title").build())
+                        .setOnSelectedListener(mockListener)
+                        .build();
+
+        OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
+
+
+        itemList.getOnSelectedListener().onSelected(0, onDoneCallback);
+        verify(mockListener).onSelected(eq(0));
+        verify(onDoneCallback).onSuccess(null);
+    }
+
+    @Test
+    public void setSelectable_disallowOnClickListenerInRows() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> builder()
+                        .addItem(Row.builder().setTitle("foo").setOnClickListener(() -> {
+                        }).build())
+                        .setOnSelectedListener((index) -> {
+                        })
+                        .build());
+
+        // Positive test.
+        builder()
+                .addItem(Row.builder().setTitle("foo").build())
+                .setOnSelectedListener((index) -> {
+                })
+                .build();
+    }
+
+    @Test
+    public void setSelectable_disallowToggleInRow() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> builder()
+                        .addItem(Row.builder().setToggle(Toggle.builder(isChecked -> {
+                        }).build()).build())
+                        .setOnSelectedListener((index) -> {
+                        })
+                        .build());
+    }
+
+    @Test
+    public void setOnItemVisibilityChangeListener_triggerListener() {
+        OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
+        ItemList list =
+                builder()
+                        .addItem(Row.builder().setTitle("1").build())
+                        .setOnItemsVisibilityChangedListener(listener)
+                        .build();
+
+        OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
+        list.getOnItemsVisibilityChangedListener().onItemVisibilityChanged(0, 1,
+                onDoneCallback);
+        ArgumentCaptor<Integer> startIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<Integer> endIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(listener).onItemVisibilityChanged(startIndexCaptor.capture(),
+                endIndexCaptor.capture());
+        verify(onDoneCallback).onSuccess(null);
+        assertThat(startIndexCaptor.getValue()).isEqualTo(0);
+        assertThat(endIndexCaptor.getValue()).isEqualTo(1);
+    }
+
+    @Test
+    public void setOnItemVisibilityChangeListener_triggerListenerWithFailure() {
+        OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
+        ItemList list =
+                builder()
+                        .addItem(Row.builder().setTitle("1").build())
+                        .setOnItemsVisibilityChangedListener(listener)
+                        .build();
+
+        String testExceptionMessage = "Test exception";
+        doThrow(new RuntimeException(testExceptionMessage)).when(listener).onItemVisibilityChanged(
+                0, 1);
+
+        OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
+        try {
+            list.getOnItemsVisibilityChangedListener().onItemVisibilityChanged(0, 1,
+                    onDoneCallback);
+        } catch (WrappedRuntimeException e) {
+            assertThat(e.getMessage()).contains(testExceptionMessage);
+        }
+
+        ArgumentCaptor<Integer> startIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<Integer> endIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(listener).onItemVisibilityChanged(startIndexCaptor.capture(),
+                endIndexCaptor.capture());
+        verify(onDoneCallback).onFailure(any());
+        assertThat(startIndexCaptor.getValue()).isEqualTo(0);
+        assertThat(endIndexCaptor.getValue()).isEqualTo(1);
+    }
+
+    @Test
+    public void equals_itemListWithRows() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                builder()
+                        .setOnSelectedListener((index) -> {
+                        })
+                        .setNoItemsMessage("no items")
+                        .setSelectedIndex(0)
+                        .setOnItemsVisibilityChangedListener((start, end) -> {
+                        })
+                        .addItem(row)
+                        .build();
+        assertThat(itemList)
+                .isEqualTo(
+                        builder()
+                                .setOnSelectedListener((index) -> {
+                                })
+                                .setNoItemsMessage("no items")
+                                .setSelectedIndex(0)
+                                .setOnItemsVisibilityChangedListener((start, end) -> {
+                                })
+                                .addItem(row)
+                                .build());
+    }
+
+    @Test
+    public void equals_itemListWithGridItems() {
+        GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
+        ItemList itemList =
+                builder()
+                        .setOnSelectedListener((index) -> {
+                        })
+                        .setNoItemsMessage("no items")
+                        .setSelectedIndex(0)
+                        .setOnItemsVisibilityChangedListener((start, end) -> {
+                        })
+                        .addItem(gridItem)
+                        .build();
+        assertThat(itemList)
+                .isEqualTo(
+                        builder()
+                                .setOnSelectedListener((index) -> {
+                                })
+                                .setNoItemsMessage("no items")
+                                .setSelectedIndex(0)
+                                .setOnItemsVisibilityChangedListener((start, end) -> {
+                                })
+                                .addItem(gridItem)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentNoItemsMessage() {
+        ItemList itemList = builder().setNoItemsMessage("no items").build();
+        assertThat(itemList).isNotEqualTo(builder().setNoItemsMessage("YO").build());
+    }
+
+    @Test
+    public void notEquals_differentSelectedIndex() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                builder().setOnSelectedListener((index) -> {
+                }).addItem(row).addItem(row).build();
+        assertThat(itemList)
+                .isNotEqualTo(
+                        builder()
+                                .setOnSelectedListener((index) -> {
+                                })
+                                .setSelectedIndex(1)
+                                .addItem(row)
+                                .addItem(row)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_missingSelectedListener() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                builder().setOnSelectedListener((index) -> {
+                }).addItem(row).addItem(row).build();
+        assertThat(itemList).isNotEqualTo(builder().addItem(row).addItem(row).build());
+    }
+
+    @Test
+    public void notEquals_missingVisibilityChangedListener() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList =
+                builder()
+                        .setOnItemsVisibilityChangedListener((start, end) -> {
+                        })
+                        .addItem(row)
+                        .addItem(row)
+                        .build();
+        assertThat(itemList).isNotEqualTo(builder().addItem(row).addItem(row).build());
+    }
+
+    @Test
+    public void notEquals_differentRows() {
+        Row row = Row.builder().setTitle("Title").build();
+        ItemList itemList = builder().addItem(row).addItem(row).build();
+        assertThat(itemList).isNotEqualTo(builder().addItem(row).build());
+    }
+
+    @Test
+    public void notEquals_differentGridItems() {
+        GridItem gridItem = GridItem.builder().setImage(BACK).setTitle("Title").build();
+        ItemList itemList = builder().addItem(gridItem).addItem(gridItem).build();
+        assertThat(itemList).isNotEqualTo(builder().addItem(gridItem).build());
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java b/car/app/app/src/test/java/androidx/car/app/model/LatLngTest.java
similarity index 91%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/LatLngTest.java
index c170d65..86b24de 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/LatLngTest.java
@@ -18,15 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link LatLng}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class LatLngTest {
     @Test
     public void createInstance() {
diff --git a/car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java
new file mode 100644
index 0000000..30d52b0
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/** Tests for {@link ListTemplate}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class ListTemplateTest {
+    @Test
+    public void createInstance_emptyList_notLoading_Throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> ListTemplate.builder().setTitle("Title").build());
+
+        // Positive case
+        ListTemplate.builder().setTitle("Title").setLoading(true).build();
+    }
+
+    @Test
+    public void createInstance_isLoading_hasList_Throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setLoading(true)
+                                .setSingleList(getList())
+                                .build());
+    }
+
+    @Test
+    public void addEmptyList_throws() {
+        ItemList emptyList = ItemList.builder().build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ListTemplate.builder().setTitle("Title").addList(emptyList,
+                        "header").build());
+    }
+
+    @Test
+    public void addList_emptyHeader_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ListTemplate.builder().setTitle("Title").addList(getList(), "").build());
+    }
+
+    @Test
+    public void addList_withVisibilityListener_throws() {
+        ItemList list =
+                ItemList.builder()
+                        .addItem(Row.builder().setTitle("Title").build())
+                        .setOnItemsVisibilityChangedListener((start, end) -> {
+                        })
+                        .build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> ListTemplate.builder().setTitle("Title").addList(list, "header").build());
+    }
+
+    @Test
+    public void addList_moreThanMaxTexts_throws() {
+        Row rowExceedsMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").addText(
+                        "text3").build();
+        Row rowMeetingMaxTexts =
+                Row.builder().setTitle("Title").addText("text1").addText("text2").build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(rowExceedsMaxTexts).build())
+                                .build());
+
+        // Positive case.
+        ListTemplate.builder()
+                .setTitle("Title")
+                .setSingleList(ItemList.builder().addItem(rowMeetingMaxTexts).build())
+                .build();
+    }
+
+    @Test
+    public void createInstance_noHeaderTitleOrAction_throws() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> ListTemplate.builder().setSingleList(getList()).build());
+
+        // Positive cases/.
+        ListTemplate.builder().setTitle("Title").setSingleList(getList()).build();
+        ListTemplate.builder().setHeaderAction(Action.BACK).setSingleList(getList()).build();
+    }
+
+    @Test
+    public void createInstance_setSingleList() {
+        ItemList list = getList();
+        ListTemplate template = ListTemplate.builder().setTitle("Title").setSingleList(
+                list).build();
+        assertThat(template.getSingleList()).isEqualTo(list);
+        assertThat(template.getSectionLists()).isEmpty();
+    }
+
+    @Test
+    public void createInstance_addList() {
+        ItemList list1 = getList();
+        ItemList list2 = getList();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .addList(list1, "header1")
+                        .addList(list2, "header2")
+                        .build();
+        assertThat(template.getSingleList()).isNull();
+        assertThat(template.getSectionLists()).hasSize(2);
+        assertThat(template.getSectionLists().get(0).getItemList()).isEqualTo(list1);
+        assertThat(template.getSectionLists().get(0).getHeader().getText()).isEqualTo("header1");
+        assertThat(template.getSectionLists().get(1).getItemList()).isEqualTo(list2);
+        assertThat(template.getSectionLists().get(1).getHeader().getText()).isEqualTo("header2");
+    }
+
+    @Test
+    public void setSingleList_clearLists() {
+        ItemList list1 = getList();
+        ItemList list2 = getList();
+        ItemList list3 = getList();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .addList(list1, "header1")
+                        .addList(list2, "header2")
+                        .setSingleList(list3)
+                        .build();
+        assertThat(template.getSingleList()).isEqualTo(list3);
+        assertThat(template.getSectionLists()).isEmpty();
+    }
+
+    @Test
+    public void addList_clearSingleList() {
+        ItemList list1 = getList();
+        ItemList list2 = getList();
+        ItemList list3 = getList();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .setSingleList(list1)
+                        .addList(list2, "header1")
+                        .addList(list3, "header2")
+                        .build();
+        assertThat(template.getSingleList()).isNull();
+        assertThat(template.getSectionLists()).hasSize(2);
+    }
+
+    @Test
+    public void createInstance_setHeaderAction_invalidActionThrows() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        ListTemplate.builder()
+                                .setHeaderAction(
+                                        Action.builder().setTitle("Action").setOnClickListener(
+                                                () -> {
+                                                }).build()));
+    }
+
+    @Test
+    public void createInstance_setHeaderAction() {
+        ListTemplate template =
+                ListTemplate.builder().setSingleList(getList()).setHeaderAction(
+                        Action.BACK).build();
+        assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
+    }
+
+    @Test
+    public void createInstance_setActionStrip() {
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .setSingleList(getList())
+                        .setActionStrip(actionStrip)
+                        .build();
+        assertThat(template.getActionStrip()).isEqualTo(actionStrip);
+    }
+
+    @Test
+    public void equals() {
+        ItemList itemList = ItemList.builder().build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+        String title = "title";
+
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setSingleList(itemList)
+                        .setActionStrip(actionStrip)
+                        .setHeaderAction(Action.BACK)
+                        .setTitle(title)
+                        .build();
+
+        assertThat(template)
+                .isEqualTo(
+                        ListTemplate.builder()
+                                .setSingleList(itemList)
+                                .setActionStrip(actionStrip)
+                                .setHeaderAction(Action.BACK)
+                                .setTitle(title)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentItemList() {
+        ItemList itemList = ItemList.builder().build();
+
+        ListTemplate template =
+                ListTemplate.builder().setTitle("Title").setSingleList(itemList).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(
+                                        ItemList.builder().addItem(
+                                                Row.builder().setTitle("Title").build()).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentHeaderAction() {
+        ItemList itemList = ItemList.builder().build();
+
+        ListTemplate template =
+                ListTemplate.builder().setSingleList(itemList).setHeaderAction(Action.BACK).build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        ListTemplate.builder()
+                                .setSingleList(itemList)
+                                .setHeaderAction(Action.APP_ICON)
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentActionStrip() {
+        ItemList itemList = ItemList.builder().build();
+        ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
+
+        ListTemplate template =
+                ListTemplate.builder()
+                        .setTitle("Title")
+                        .setSingleList(itemList)
+                        .setActionStrip(actionStrip)
+                        .build();
+
+        assertThat(template)
+                .isNotEqualTo(
+                        ListTemplate.builder()
+                                .setTitle("Title")
+                                .setSingleList(itemList)
+                                .setActionStrip(
+                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
+                                .build());
+    }
+
+    @Test
+    public void notEquals_differentTitle() {
+        ItemList itemList = ItemList.builder().build();
+        String title = "title";
+
+        ListTemplate template = ListTemplate.builder().setSingleList(itemList).setTitle(
+                title).build();
+
+        assertThat(template)
+                .isNotEqualTo(ListTemplate.builder().setSingleList(itemList).setTitle(
+                        "yo").build());
+    }
+
+    private static ItemList getList() {
+        Row row1 = Row.builder().setTitle("Bananas").build();
+        Row row2 = Row.builder().setTitle("Oranges").build();
+        return ItemList.builder().addItem(row1).addItem(row2).build();
+    }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
similarity index 81%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
index 4784044..eb791af 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
@@ -26,21 +26,18 @@
 import android.net.Uri;
 import android.util.Log;
 
-import androidx.car.app.test.R;
-import androidx.car.app.utils.Logger;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import com.google.common.collect.ImmutableList;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link MessageTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class MessageTemplateTest {
 
     private final String mTitle = "header";
@@ -85,7 +82,7 @@
         assertThat(template.getTitle().getText()).isEqualTo("header");
         assertThat(template.getIcon()).isNull();
         assertThat(template.getHeaderAction()).isNull();
-        assertThat(template.getActionList()).isNull();
+        assertThat(template.getActions()).isNull();
         assertThat(template.getDebugMessage()).isNull();
     }
 
@@ -124,58 +121,7 @@
                 Log.getStackTraceString(exception));
         assertThat(template.getIcon()).isEqualTo(icon);
         assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
-        assertThat(template.getActionList().getList()).containsExactly(action);
-    }
-
-    @Test
-    public void validate_isRefresh() {
-        Logger logger = message -> {
-        };
-        MessageTemplate template = MessageTemplate.builder(mMessage).setTitle(mTitle).build();
-
-        assertThat(template.isRefresh(template, logger)).isTrue();
-
-        // Allowed mutable fields: icon, action strip and actions.
-        Action action = Action.builder().setOnClickListener(() -> {
-        }).setTitle("foo").build();
-        assertThat(
-                template.isRefresh(
-                        MessageTemplate.builder(mMessage)
-                                .setTitle(mTitle)
-                                .setIcon(
-                                        CarIcon.of(
-                                                IconCompat.createWithResource(
-                                                        ApplicationProvider.getApplicationContext(),
-                                                        R.drawable.ic_test_1)))
-                                .setHeaderAction(Action.BACK)
-                                .setActions(ImmutableList.of(action))
-                                .build(),
-                        logger))
-                .isTrue();
-
-        // Text changes are disallowed.
-        assertThat(
-                template.isRefresh(MessageTemplate.builder("Message2").setTitle(mTitle).build(),
-                        logger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        MessageTemplate.builder(mMessage).setTitle(mTitle).setDebugMessage(
-                                "Debug").build(),
-                        logger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        MessageTemplate.builder(mMessage)
-                                .setTitle(mTitle)
-                                .setDebugCause(new IllegalArgumentException("Exception"))
-                                .build(),
-                        logger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        MessageTemplate.builder(mMessage).setTitle("Header2").build(), logger))
-                .isFalse();
+        assertThat(template.getActions().getList()).containsExactly(action);
     }
 
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java b/car/app/app/src/test/java/androidx/car/app/model/MetadataTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/MetadataTest.java
index a67ff2d..e3310f11 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/MetadataTest.java
@@ -18,15 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for the {@link Metadata} class. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class MetadataTest {
     @Test
     public void setAndGetPlace() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java b/car/app/app/src/test/java/androidx/car/app/model/ModelUtilsTest.java
similarity index 79%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ModelUtilsTest.java
index 4a82bff..bbf190f 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ModelUtilsTest.java
@@ -20,26 +20,24 @@
 
 import android.text.SpannableString;
 
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
+import androidx.car.app.TestUtils;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import com.google.common.collect.ImmutableList;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link PlaceListMapTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ModelUtilsTest {
     @Test
     public void validateAllNonBrowsableRowsHaveDistances() {
-        DistanceSpan span =
-                DistanceSpan.create(
-                        Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+        DistanceSpan span = DistanceSpan.create(
+                Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
         SpannableString stringWithDistance = new SpannableString("Test");
         stringWithDistance.setSpan(span, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
         SpannableString stringWithInvalidDistance = new SpannableString("Test");
@@ -56,14 +54,12 @@
 
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        ModelUtils.validateAllNonBrowsableRowsHaveDistance(
-                                ImmutableList.of(rowWithDistance, rowWithInvalidDistance)));
+                () -> ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+                        ImmutableList.of(rowWithDistance, rowWithInvalidDistance)));
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        ModelUtils.validateAllNonBrowsableRowsHaveDistance(
-                                ImmutableList.of(rowWithDistance, rowWithoutDistance)));
+                () -> ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+                        ImmutableList.of(rowWithDistance, rowWithoutDistance)));
 
         // Positive cases
         ModelUtils.validateAllNonBrowsableRowsHaveDistance(ImmutableList.of());
@@ -102,14 +98,12 @@
 
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        ModelUtils.validateAllRowsHaveDistanceOrDuration(
-                                ImmutableList.of(rowWithDuration, rowWithInvalidDuration)));
+                () -> ModelUtils.validateAllRowsHaveDistanceOrDuration(
+                        ImmutableList.of(rowWithDuration, rowWithInvalidDuration)));
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        ModelUtils.validateAllRowsHaveDistanceOrDuration(
-                                ImmutableList.of(rowWithDuration, plainRow)));
+                () -> ModelUtils.validateAllRowsHaveDistanceOrDuration(
+                        ImmutableList.of(rowWithDuration, plainRow)));
 
         // Positive cases.
         ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of());
@@ -123,10 +117,8 @@
 
     @Test
     public void validateAllRowsHaveOnlySmallSizedImages() {
-        CarIcon carIcon =
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
         Row rowWithNoImage = Row.builder().setTitle("title1").build();
         Row rowWithSmallImage =
                 Row.builder().setTitle("title2").setImage(carIcon, Row.IMAGE_TYPE_SMALL).build();
@@ -139,9 +131,8 @@
                         ImmutableList.of(rowWithLargeImage)));
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        ModelUtils.validateAllRowsHaveOnlySmallImages(
-                                ImmutableList.of(rowWithNoImage, rowWithLargeImage)));
+                () -> ModelUtils.validateAllRowsHaveOnlySmallImages(
+                        ImmutableList.of(rowWithNoImage, rowWithLargeImage)));
 
         // Positive cases
         ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of());
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/host/model/OnClickListenerWrapperTest.java b/car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
similarity index 82%
rename from car/app/app/src/androidTest/java/androidx/car/app/host/model/OnClickListenerWrapperTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
index 889e339..3afb7ee 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/host/model/OnClickListenerWrapperTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.host.model;
+package androidx.car.app.model;
 
 /** Tests for {@link OnClickListenerWrapper}. */
 
@@ -23,11 +23,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.model.OnClickListener;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.car.app.OnDoneCallback;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -35,9 +31,11 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class OnClickListenerWrapperTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
@@ -46,7 +44,6 @@
     OnClickListener mMockOnClickListener;
 
     @Test
-    @UiThreadTest
     public void create() {
         OnClickListenerWrapper wrapper = OnClickListenerWrapperImpl.create(mMockOnClickListener);
         assertThat(wrapper.isParkedOnly()).isFalse();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
similarity index 67%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
index 6275cd6..80f0bf3 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
@@ -20,22 +20,16 @@
 
 import static org.junit.Assert.assertThrows;
 
-import android.text.SpannableString;
-
 import androidx.car.app.TestUtils;
-import androidx.car.app.test.R;
-import androidx.car.app.utils.Logger;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link PaneTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PaneTemplateTest {
 
     @Test
@@ -141,94 +135,6 @@
     }
 
     @Test
-    public void validate_isRefresh() {
-        Logger logger = message -> {
-        };
-        Row.Builder row = Row.builder().setTitle("Row1");
-        PaneTemplate template =
-                PaneTemplate.builder(Pane.builder().addRow(row.build()).build()).setTitle(
-                        "Title").build();
-
-        assertThat(template.isRefresh(template, logger)).isTrue();
-
-        // Going from loading state to new content is allowed.
-        assertThat(
-                template.isRefresh(
-                        PaneTemplate.builder(Pane.builder().setLoading(true).build())
-                                .setTitle("Title")
-                                .build(),
-                        logger))
-                .isTrue();
-
-        // Other allowed mutable states.
-        SpannableString stringWithSpan = new SpannableString("Row1");
-        stringWithSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
-        IconCompat icon = IconCompat.createWithResource(
-                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
-        assertThat(
-                template.isRefresh(
-                        PaneTemplate.builder(
-                                Pane.builder()
-                                        .addRow(
-                                                row.setImage(CarIcon.of(icon))
-                                                        .setTitle(stringWithSpan)
-                                                        .build())
-                                        .build())
-                                .setTitle("Title")
-                                .setHeaderAction(Action.BACK)
-                                .setActionStrip(
-                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
-                                .build(),
-                        logger))
-                .isTrue();
-
-        // Title updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        PaneTemplate.builder(Pane.builder().addRow(row.build()).build())
-                                .setTitle("Title2")
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Text updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        PaneTemplate.builder(
-                                Pane.builder().addRow(row.setTitle("Row2").build()).build())
-                                .setTitle("Title")
-                                .build(),
-                        logger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        PaneTemplate.builder(
-                                Pane.builder().addRow(row.addText("Text").build()).build())
-                                .setTitle("Title")
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Additional rows are disallowed.
-        assertThat(
-                template.isRefresh(
-                        PaneTemplate.builder(Pane.builder().addRow(row.build()).addRow(
-                                row.build()).build())
-                                .setTitle("Title")
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Going from content to loading state is disallowed.
-        assertThat(
-                PaneTemplate.builder(Pane.builder().setLoading(true).build())
-                        .setTitle("Title")
-                        .build()
-                        .isRefresh(template, logger))
-                .isFalse();
-    }
-
-    @Test
     public void equals() {
         Pane pane = Pane.builder().addRow(Row.builder().setTitle("Title").build()).build();
         ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java b/car/app/app/src/test/java/androidx/car/app/model/PaneTest.java
similarity index 61%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PaneTest.java
index 2531bd6..9fdb2f3 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PaneTest.java
@@ -20,23 +20,19 @@
 
 import static org.junit.Assert.assertThrows;
 
-import android.text.SpannableString;
-
-import androidx.car.app.utils.Logger;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import com.google.common.collect.ImmutableList;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.Arrays;
 import java.util.List;
 
 /** Tests for {@link Pane}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PaneTest {
     @Test
     public void createEmptyRows_throws() {
@@ -65,13 +61,6 @@
     }
 
     @Test
-    public void clearRows() {
-        Row row = createRow(1);
-        Pane pane = Pane.builder().addRow(row).addRow(row).clearRows().addRow(row).build();
-        assertThat(pane.getRows()).hasSize(1);
-    }
-
-    @Test
     public void addRow_multiple() {
         Row row1 = createRow(1);
         Row row2 = createRow(2);
@@ -88,7 +77,7 @@
         Pane pane =
                 Pane.builder().addRow(Row.builder().setTitle("Title").build()).setActions(
                         actions).build();
-        assertActions(pane.getActionList(), actions);
+        assertActions(pane.getActions(), actions);
     }
 
     @Test
@@ -100,67 +89,6 @@
     }
 
     @Test
-    public void validate_isRefresh() {
-        Logger logger = message -> {
-        };
-        Row.Builder row = Row.builder().setTitle("Title1");
-
-        Pane.Builder builder = Pane.builder().setLoading(true);
-        Pane pane = builder.build();
-        assertThat(pane.isRefresh(builder.build(), logger)).isTrue();
-
-        // Going from loading state to new content is allowed.
-        Pane paneWithRows = Pane.builder().addRow(row.build()).build();
-        assertThat(paneWithRows.isRefresh(pane, logger)).isTrue();
-
-        // Text updates are disallowed.
-        Pane paneWithDifferentTitle = Pane.builder().addRow(row.setTitle("Title2").build()).build();
-        Pane paneWithDifferentText = Pane.builder().addRow(row.addText("Text").build()).build();
-        assertThat(paneWithDifferentTitle.isRefresh(paneWithRows, logger)).isFalse();
-        assertThat(paneWithDifferentText.isRefresh(paneWithRows, logger)).isFalse();
-
-        // Additional rows are disallowed.
-        Pane paneWithTwoRows = Pane.builder().addRow(row.build()).addRow(row.build()).build();
-        assertThat(paneWithTwoRows.isRefresh(paneWithRows, logger)).isFalse();
-
-        // Going from content to loading state is disallowed.
-        assertThat(pane.isRefresh(paneWithRows, logger)).isFalse();
-    }
-
-    @Test
-    public void validate_isRefresh_differentSpansAreIgnored() {
-        Logger logger = message -> {
-        };
-        SpannableString textWithDistanceSpan = new SpannableString("Text");
-        textWithDistanceSpan.setSpan(
-                DistanceSpan.create(Distance.create(1000, Distance.UNIT_KILOMETERS)),
-                /* start= */ 0,
-                /* end= */ 1,
-                /* flags= */ 0);
-        SpannableString textWithDurationSpan = new SpannableString("Text");
-        textWithDurationSpan.setSpan(DurationSpan.create(1), 0, /* end= */ 1, /* flags= */ 0);
-
-        Pane pane1 =
-                Pane.builder()
-                        .addRow(
-                                Row.builder().setTitle(textWithDistanceSpan).addText(
-                                        textWithDurationSpan).build())
-                        .build();
-        Pane pane2 =
-                Pane.builder()
-                        .addRow(
-                                Row.builder().setTitle(textWithDurationSpan).addText(
-                                        textWithDistanceSpan).build())
-                        .build();
-        Pane pane3 =
-                Pane.builder().addRow(Row.builder().setTitle("Text2").addText(
-                        "Text2").build()).build();
-
-        assertThat(pane2.isRefresh(pane1, logger)).isTrue();
-        assertThat(pane3.isRefresh(pane1, logger)).isFalse();
-    }
-
-    @Test
     public void equals() {
         Pane pane =
                 Pane.builder()
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java b/car/app/app/src/test/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
similarity index 83%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
index 4880a6d..d871996 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
@@ -23,12 +23,7 @@
 
 import android.os.RemoteException;
 
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.host.model.OnClickListenerWrapper;
-import androidx.car.app.host.model.OnClickListenerWrapperImpl;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.car.app.OnDoneCallback;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -36,10 +31,12 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link OnClickListenerWrapper}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ParkedOnlyOnClickListenerTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
@@ -48,7 +45,6 @@
     OnClickListener mMockOnClickListener;
 
     @Test
-    @UiThreadTest
     public void create() throws RemoteException {
         ParkedOnlyOnClickListener parkedOnlyOnClickListener =
                 ParkedOnlyOnClickListener.create(mMockOnClickListener);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/PlaceListMapTemplateTest.java
similarity index 79%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PlaceListMapTemplateTest.java
index 16257d0..d590b0b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PlaceListMapTemplateTest.java
@@ -24,19 +24,16 @@
 import android.text.SpannableString;
 
 import androidx.car.app.TestUtils;
-import androidx.car.app.test.R;
-import androidx.car.app.utils.Logger;
-import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link PlaceListMapTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PlaceListMapTemplateTest {
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private final DistanceSpan mDistanceSpan =
@@ -287,121 +284,6 @@
     }
 
     @Test
-    public void validate_isRefresh() {
-        Logger logger = message -> {
-        };
-        SpannableString title = new SpannableString("Title");
-        title.setSpan(mDistanceSpan, 0, 1, 0);
-        Row.Builder row =
-                Row.builder().setTitle(title).setBrowsable(true).setOnClickListener(() -> {
-                });
-        PlaceListMapTemplate template =
-                PlaceListMapTemplate.builder()
-                        .setTitle("Title")
-                        .setItemList(ItemList.builder().addItem(row.build()).build())
-                        .build();
-
-        assertThat(template.isRefresh(template, logger)).isTrue();
-
-        // Going from loading state to new content is allowed.
-        assertThat(
-                template.isRefresh(
-                        PlaceListMapTemplate.builder().setTitle("Title").setLoading(true).build(),
-                        logger))
-                .isTrue();
-
-        // Other allowed mutable states.
-        SpannableString stringWithSpan = new SpannableString("Title");
-        stringWithSpan.setSpan(mDistanceSpan, 1, /* end= */ 2, /* flags= */ 0);
-        ItemList itemList = ItemList.builder()
-                .addItem(
-                        row.setOnClickListener(() -> {
-                        })
-                                .setBrowsable(false)
-                                .setTitle(stringWithSpan)
-                                .setImage(
-                                        CarIcon.of(
-                                                IconCompat.createWithResource(
-                                                        ApplicationProvider.getApplicationContext(),
-                                                        R.drawable.ic_test_1)))
-                                .setMetadata(
-                                        Metadata.ofPlace(
-                                                Place.builder(
-                                                        LatLng.create(
-                                                                1,
-                                                                1)).build()))
-                                .build())
-                .build();
-        assertThat(
-                template.isRefresh(
-                        PlaceListMapTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(itemList)
-                                .setHeaderAction(Action.BACK)
-                                .setActionStrip(
-                                        ActionStrip.builder().addAction(Action.APP_ICON).build())
-                                .build(),
-                        logger))
-                .isTrue();
-
-        // Title updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        PlaceListMapTemplate.builder()
-                                .setItemList(ItemList.builder().addItem(row.build()).build())
-                                .setTitle("Title2")
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Text updates are disallowed.
-        SpannableString title2 = new SpannableString("Title2");
-        title2.setSpan(mDistanceSpan, 0, 1, 0);
-        assertThat(
-                template.isRefresh(
-                        PlaceListMapTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(ItemList.builder().addItem(
-                                        row.setTitle(title2).build()).build())
-                                .build(),
-                        logger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        PlaceListMapTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(
-                                        ItemList.builder()
-                                                .addItem(row.setTitle(title).addText(
-                                                        "Text").build())
-                                                .build())
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Additional rows are disallowed.
-        assertThat(
-                template.isRefresh(
-                        PlaceListMapTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(
-                                        ItemList.builder().addItem(row.build()).addItem(
-                                                row.build()).build())
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Going from content to loading state is disallowed.
-        assertThat(
-                PlaceListMapTemplate.builder()
-                        .setTitle("Title")
-                        .setLoading(true)
-                        .build()
-                        .isRefresh(template, logger))
-                .isFalse();
-    }
-
-    @Test
     public void equals() {
         ActionStrip actionStrip = ActionStrip.builder().addAction(Action.BACK).build();
         String title = "foo";
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java b/car/app/app/src/test/java/androidx/car/app/model/PlaceMarkerTest.java
similarity index 67%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PlaceMarkerTest.java
index 3e7cc36..41a25e1 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PlaceMarkerTest.java
@@ -26,18 +26,18 @@
 import android.content.ContentResolver;
 import android.net.Uri;
 
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link PlaceMarker}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PlaceMarkerTest {
 
     @Test
@@ -48,17 +48,14 @@
 
     @Test
     public void setColor_withImageTypeIcon_throws() {
-        CarIcon icon =
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        CarIcon icon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
         assertThrows(
                 IllegalStateException.class,
-                () ->
-                        PlaceMarker.builder()
-                                .setIcon(icon, PlaceMarker.TYPE_IMAGE)
-                                .setColor(CarColor.SECONDARY)
-                                .build());
+                () -> PlaceMarker.builder()
+                        .setIcon(icon, PlaceMarker.TYPE_IMAGE)
+                        .setColor(CarColor.SECONDARY)
+                        .build());
     }
 
     @Test
@@ -75,16 +72,13 @@
 
     @Test
     public void createInstance() {
-        CarIcon icon =
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
-        PlaceMarker marker1 =
-                PlaceMarker.builder()
-                        .setIcon(icon, PlaceMarker.TYPE_ICON)
-                        .setLabel("foo")
-                        .setColor(CarColor.SECONDARY)
-                        .build();
+        CarIcon icon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
+        PlaceMarker marker1 = PlaceMarker.builder()
+                .setIcon(icon, PlaceMarker.TYPE_ICON)
+                .setLabel("foo")
+                .setColor(CarColor.SECONDARY)
+                .build();
         assertThat(marker1.getIcon()).isEqualTo(icon);
         assertThat(marker1.getIconType()).isEqualTo(PlaceMarker.TYPE_ICON);
         assertThat(marker1.getColor()).isEqualTo(CarColor.SECONDARY);
@@ -103,23 +97,19 @@
 
     @Test
     public void equals() {
-        CarIcon carIcon =
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
-        PlaceMarker marker =
-                PlaceMarker.builder()
-                        .setIcon(carIcon, PlaceMarker.TYPE_ICON)
-                        .setLabel("foo")
-                        .setColor(CarColor.SECONDARY)
-                        .build();
+        CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
+        PlaceMarker marker = PlaceMarker.builder()
+                .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+                .setLabel("foo")
+                .setColor(CarColor.SECONDARY)
+                .build();
 
-        assertThat(
-                PlaceMarker.builder()
-                        .setIcon(carIcon, PlaceMarker.TYPE_ICON)
-                        .setLabel("foo")
-                        .setColor(CarColor.SECONDARY)
-                        .build())
+        assertThat(PlaceMarker.builder()
+                .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+                .setLabel("foo")
+                .setColor(CarColor.SECONDARY)
+                .build())
                 .isEqualTo(marker);
     }
 
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java b/car/app/app/src/test/java/androidx/car/app/model/PlaceTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PlaceTest.java
index 664dd26..d4ef129 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PlaceTest.java
@@ -18,15 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for the {@link Place} class. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PlaceTest {
     /** Tests basic setter and getter operations. */
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
similarity index 82%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/RowTest.java
index c60b39e..69c140b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
@@ -25,20 +25,18 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import androidx.car.app.host.OnDoneCallback;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.TestUtils;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Row}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class RowTest {
     @Test
     public void create_defaultValues() {
@@ -60,22 +58,7 @@
     }
 
     @Test
-    public void title_carText() {
-        CarText title = CarText.create("foo");
-        Row row = Row.builder().setTitle(title).build();
-        assertThat(title).isEqualTo(row.getTitle());
-    }
-
-    @Test
     public void text_charSequence() {
-        CarText text1 = CarText.create("foo");
-        CarText text2 = CarText.create("bar");
-        Row row = Row.builder().setTitle("Title").addText(text1).addText(text2).build();
-        assertThat(row.getTexts()).containsExactly(text1, text2);
-    }
-
-    @Test
-    public void text_carText() {
         String text1 = "foo";
         String text2 = "bar";
         Row row = Row.builder().setTitle("Title").addText(text1).addText(text2).build();
@@ -98,14 +81,6 @@
     }
 
     @Test
-    public void setSectionHeader() {
-        Row row =
-                Row.builder().setFlags(Row.ROW_FLAG_SECTION_HEADER).setTitle(
-                        "section header").build();
-        assertThat(row.getFlags() & Row.ROW_FLAG_SECTION_HEADER).isNotEqualTo(0);
-    }
-
-    @Test
     public void setOnClickListenerAndToggle_throws() {
         Toggle toggle1 = Toggle.builder(isChecked -> {
         }).build();
@@ -121,7 +96,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void clickListener() {
         OnClickListener onClickListener = mock(OnClickListener.class);
         Row row = Row.builder().setTitle("Title").setOnClickListener(onClickListener).build();
@@ -168,11 +142,8 @@
                 })
                 .setTitle("Title")
                 .addText("Text")
-                .setImage(
-                        CarIcon.of(
-                                IconCompat.createWithResource(
-                                        ApplicationProvider.getApplicationContext(),
-                                        R.drawable.ic_test_1)))
+                .setImage(TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                        "ic_test_1"))
                 .build();
     }
 
@@ -187,7 +158,6 @@
                         .setOnClickListener(() -> {
                         })
                         .setBrowsable(false)
-                        .setFlags(1)
                         .setMetadata(Metadata.EMPTY_METADATA)
                         .addText(title)
                         .build();
@@ -199,7 +169,6 @@
                         .setOnClickListener(() -> {
                         })
                         .setBrowsable(false)
-                        .setFlags(1)
                         .setMetadata(Metadata.EMPTY_METADATA)
                         .addText(title)
                         .build())
@@ -247,13 +216,6 @@
     }
 
     @Test
-    public void notEquals_differentFlags() {
-        Row row = Row.builder().setTitle("Title").setFlags(1).build();
-
-        assertThat(Row.builder().setTitle("Title").setFlags(2).build()).isNotEqualTo(row);
-    }
-
-    @Test
     public void notEquals_differentMetadata() {
         Row row = Row.builder().setTitle("Title").setMetadata(Metadata.EMPTY_METADATA).build();
 
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/SearchTemplateTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/SearchTemplateTest.java
index 16c52ba..9126708 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/SearchTemplateTest.java
@@ -26,13 +26,9 @@
 
 import android.os.RemoteException;
 
-import androidx.car.app.SearchListener;
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.TestUtils;
 import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnDoneCallback;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -40,16 +36,18 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link SearchTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class SearchTemplateTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
     @Mock
-    SearchListener mMockSearchListener;
+    SearchTemplate.SearchListener mMockSearchListener;
 
     @Test
     public void createInstance_isLoading_hasList_Throws() {
@@ -124,7 +122,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void buildWithValues() throws RemoteException {
         String initialSearchText = "searchTemplate for this!!";
         String searchHint = "This is not a hint";
@@ -155,7 +152,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void buildWithValues_failureOnSearchSubmitted() throws RemoteException {
         String initialSearchText = "searchTemplate for this!!";
         String searchHint = "This is not a hint";
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java b/car/app/app/src/test/java/androidx/car/app/model/SectionedItemListTest.java
similarity index 94%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/SectionedItemListTest.java
index 69f5124..b80e38a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/SectionedItemListTest.java
@@ -18,15 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link ItemListTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class SectionedItemListTest {
 
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java b/car/app/app/src/test/java/androidx/car/app/model/TemplateWrapperTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/TemplateWrapperTest.java
index 3cbb446..f8ff387 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/TemplateWrapperTest.java
@@ -18,15 +18,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link TemplateWrapper}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class TemplateWrapperTest {
     @Test
     public void createInstance() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java b/car/app/app/src/test/java/androidx/car/app/model/ToggleTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ToggleTest.java
index 5f7aca2..d4d3575 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ToggleTest.java
@@ -23,12 +23,9 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.WrappedRuntimeException;
-import androidx.car.app.host.OnDoneCallback;
 import androidx.car.app.model.Toggle.OnCheckedChangeListener;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -36,10 +33,12 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Toggle}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ToggleTest {
     @Rule
     public MockitoRule mocks = MockitoJUnit.rule();
@@ -54,7 +53,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void build_checkedChange_sendsCheckedChangeCall() {
         Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build();
         OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
@@ -65,7 +63,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void build_checkedChange_sendsCheckedChangeCallWithFailure() {
         String testExceptionMessage = "Test exception";
         doThrow(new RuntimeException(testExceptionMessage)).when(
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
similarity index 63%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
index ab577b5..0ac564e 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
@@ -24,20 +24,18 @@
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarIcon;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.Collections;
 
 /** Tests for {@link ActionsConstraints}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ActionsConstraintsTest {
     @Test
     public void createEmpty() {
@@ -51,23 +49,21 @@
     public void create_requiredExceedsMaxAllowedActions() {
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        ActionsConstraints.builder()
-                                .setMaxActions(1)
-                                .addRequiredActionType(Action.TYPE_BACK)
-                                .addRequiredActionType(Action.TYPE_CUSTOM)
-                                .build());
+                () -> ActionsConstraints.builder()
+                        .setMaxActions(1)
+                        .addRequiredActionType(Action.TYPE_BACK)
+                        .addRequiredActionType(Action.TYPE_CUSTOM)
+                        .build());
     }
 
     @Test
     public void create_requiredAlsoDisallowed() {
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        ActionsConstraints.builder()
-                                .addRequiredActionType(Action.TYPE_BACK)
-                                .addDisallowedActionType(Action.TYPE_BACK)
-                                .build());
+                () -> ActionsConstraints.builder()
+                        .addRequiredActionType(Action.TYPE_BACK)
+                        .addDisallowedActionType(Action.TYPE_BACK)
+                        .build());
     }
 
     @Test
@@ -94,10 +90,8 @@
                         .addDisallowedActionType(Action.TYPE_BACK)
                         .build();
 
-        CarIcon carIcon =
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
         Action actionWithIcon = TestUtils.createAction(null, carIcon);
         Action actionWithTitle = TestUtils.createAction("Title", carIcon);
 
@@ -115,39 +109,35 @@
         // Missing required type.
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                ActionStrip.builder().addAction(
-                                        Action.APP_ICON).build().getActions()));
+                () -> constraints.validateOrThrow(
+                        ActionStrip.builder().addAction(
+                                Action.APP_ICON).build().getActions()));
 
         // Disallowed type
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                ActionStrip.builder().addAction(Action.BACK).build().getActions()));
+                () -> constraints.validateOrThrow(
+                        ActionStrip.builder().addAction(Action.BACK).build().getActions()));
 
         // Over max allowed actions
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                ActionStrip.builder()
-                                        .addAction(Action.APP_ICON)
-                                        .addAction(actionWithIcon)
-                                        .addAction(actionWithTitle)
-                                        .build()
-                                        .getActions()));
+                () -> constraints.validateOrThrow(
+                        ActionStrip.builder()
+                                .addAction(Action.APP_ICON)
+                                .addAction(actionWithIcon)
+                                .addAction(actionWithTitle)
+                                .build()
+                                .getActions()));
 
         // Over max allowed actions with title
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                ActionStrip.builder()
-                                        .addAction(actionWithTitle)
-                                        .addAction(actionWithTitle)
-                                        .build()
-                                        .getActions()));
+                () -> constraints.validateOrThrow(
+                        ActionStrip.builder()
+                                .addAction(actionWithTitle)
+                                .addAction(actionWithTitle)
+                                .build()
+                                .getActions()));
     }
 }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowConstraintsTest.java
similarity index 66%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/constraints/RowConstraintsTest.java
index 9b4d96e..5bc88c5 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowConstraintsTest.java
@@ -18,21 +18,20 @@
 
 import static org.junit.Assert.assertThrows;
 
+import androidx.car.app.TestUtils;
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Toggle;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link RowConstraints}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class RowConstraintsTest {
     @Test
     public void validate_clickListener() {
@@ -43,10 +42,9 @@
 
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                Row.builder().setTitle("Title)").setOnClickListener(() -> {
-                                }).build()));
+                () -> constraints.validateOrThrow(
+                        Row.builder().setTitle("Title)").setOnClickListener(() -> {
+                        }).build()));
 
         // Positive cases
         constraints.validateOrThrow(Row.builder().setTitle("Title").build());
@@ -62,13 +60,12 @@
 
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                Row.builder()
-                                        .setTitle("Title)")
-                                        .setToggle(Toggle.builder(isChecked -> {
-                                        }).build())
-                                        .build()));
+                () -> constraints.validateOrThrow(
+                        Row.builder()
+                                .setTitle("Title)")
+                                .setToggle(Toggle.builder(isChecked -> {
+                                }).build())
+                                .build()));
 
         // Positive cases
         constraints.validateOrThrow(Row.builder().setTitle("Title").build());
@@ -81,16 +78,13 @@
     public void validate_images() {
         RowConstraints constraints = RowConstraints.builder().setImageAllowed(false).build();
         RowConstraints allowConstraints = RowConstraints.builder().setImageAllowed(true).build();
-        CarIcon carIcon =
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
 
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                Row.builder().setTitle("Title)").setImage(carIcon).build()));
+                () -> constraints.validateOrThrow(
+                        Row.builder().setTitle("Title)").setImage(carIcon).build()));
 
         // Positive cases
         constraints.validateOrThrow(Row.builder().setTitle("Title").build());
@@ -103,14 +97,13 @@
 
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        constraints.validateOrThrow(
-                                Row.builder()
-                                        .setTitle("Title)")
-                                        .addText("text1")
-                                        .addText("text2")
-                                        .addText("text3")
-                                        .build()));
+                () -> constraints.validateOrThrow(
+                        Row.builder()
+                                .setTitle("Title)")
+                                .addText("text1")
+                                .addText("text2")
+                                .addText("text3")
+                                .build()));
 
         // Positive cases
         constraints.validateOrThrow(
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
index 56b5273..131dd52 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
@@ -19,15 +19,15 @@
 import static org.junit.Assert.assertThrows;
 
 import androidx.car.app.TestUtils;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link RowListConstraints}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class RowListConstraintsTest {
     @Test
     public void validate_itemList_noSelectable() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
similarity index 78%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
index 051358e..6605c9b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
@@ -37,9 +37,8 @@
 import androidx.car.app.navigation.model.TravelEstimate;
 import androidx.car.app.navigation.model.Trip;
 import androidx.car.app.serialization.Bundleable;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.car.app.testing.TestCarContext;
+import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -47,12 +46,15 @@
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
 /** Tests for {@link NavigationManager}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class NavigationManagerTest {
     @Mock
     private ICarHost mMockCarHost;
@@ -88,10 +90,12 @@
                     .build();
 
     @Before
-    @UiThreadTest
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
 
+        TestCarContext testCarContext =
+                TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
+
         INavigationHost navHostStub =
                 new INavigationHost.Stub() {
                     @Override
@@ -113,15 +117,14 @@
 
         mHostDispatcher.setCarHost(mMockCarHost);
 
-        mNavigationManager = NavigationManager.create(mHostDispatcher);
+        mNavigationManager = NavigationManager.create(testCarContext, mHostDispatcher);
     }
 
     @Test
-    @UiThreadTest
     public void navigationStarted_sendState_navigationEnded() throws RemoteException {
         InOrder inOrder = inOrder(mMockNavHost);
 
-        mNavigationManager.setListener(mNavigationListener);
+        mNavigationManager.setNavigationManagerListener(mNavigationListener);
         mNavigationManager.navigationStarted();
         inOrder.verify(mMockNavHost).navigationStarted();
 
@@ -133,16 +136,14 @@
     }
 
     @Test
-    @UiThreadTest
     public void navigationStarted_noListenerSet() throws RemoteException {
         assertThrows(IllegalStateException.class, () -> mNavigationManager.navigationStarted());
     }
 
     @Test
-    @UiThreadTest
     public void navigationStarted_multiple() throws RemoteException {
 
-        mNavigationManager.setListener(mNavigationListener);
+        mNavigationManager.setNavigationManagerListener(mNavigationListener);
         mNavigationManager.navigationStarted();
 
         mNavigationManager.navigationStarted();
@@ -150,7 +151,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void navigationEnded_multiple_not_started() throws RemoteException {
         mNavigationManager.navigationEnded();
         mNavigationManager.navigationEnded();
@@ -164,44 +164,52 @@
     }
 
     @Test
-    @UiThreadTest
-    public void stopNavigation_notNavigating() throws RemoteException {
-        mNavigationManager.setListener(mNavigationListener);
-        mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
-        verify(mNavigationListener, never()).stopNavigation();
+    public void onStopNavigation_notNavigating() throws RemoteException {
+        mNavigationManager.setNavigationManagerListener(mNavigationListener);
+        mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+        verify(mNavigationListener, never()).onStopNavigation();
     }
 
     @Test
-    @UiThreadTest
-    public void stopNavigation_navigating_restart() throws RemoteException {
+    public void onStopNavigation_navigating_restart() throws RemoteException {
         InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
 
-        mNavigationManager.setListener(mNavigationListener);
+        mNavigationManager.setNavigationManagerListener(new SynchronousExecutor(),
+                mNavigationListener);
         mNavigationManager.navigationStarted();
         inOrder.verify(mMockNavHost).navigationStarted();
 
-        mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
-        inOrder.verify(mNavigationListener).stopNavigation();
+        mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
+        inOrder.verify(mNavigationListener).onStopNavigation();
 
         mNavigationManager.navigationStarted();
         inOrder.verify(mMockNavHost).navigationStarted();
     }
 
     @Test
-    @UiThreadTest
     public void onAutoDriveEnabled_callsListener() {
-        mNavigationManager.setListener(mNavigationListener);
+        mNavigationManager.setNavigationManagerListener(new SynchronousExecutor(),
+                mNavigationListener);
         mNavigationManager.onAutoDriveEnabled();
 
         verify(mNavigationListener).onAutoDriveEnabled();
     }
 
     @Test
-    @UiThreadTest
     public void onAutoDriveEnabledBeforeRegisteringListener_callsListener() {
         mNavigationManager.onAutoDriveEnabled();
-        mNavigationManager.setListener(mNavigationListener);
+        mNavigationManager.setNavigationManagerListener(new SynchronousExecutor(),
+                mNavigationListener);
 
         verify(mNavigationListener).onAutoDriveEnabled();
     }
+
+    static class SynchronousExecutor implements Executor {
+        @Override
+        public void execute(Runnable r) {
+            r.run();
+        }
+    }
+
 }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/DestinationTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/DestinationTest.java
index ba52c24..0663a13 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/DestinationTest.java
@@ -25,15 +25,15 @@
 
 import androidx.car.app.model.CarIcon;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Destination}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class DestinationTest {
 
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneDirectionTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/LaneDirectionTest.java
index 3f17b37..3d691ec 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneDirectionTest.java
@@ -20,15 +20,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link LaneDirection}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class LaneDirectionTest {
 
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneTest.java
similarity index 94%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/LaneTest.java
index 8d4a1197..aee2408 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneTest.java
@@ -21,15 +21,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Lane}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class LaneTest {
 
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/ManeuverTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/ManeuverTest.java
index 390b2ee..5064bda 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/ManeuverTest.java
@@ -17,11 +17,11 @@
 package androidx.car.app.navigation.model;
 
 import static androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT;
-import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER;
 import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
 import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE;
 import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW;
 import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE;
+import static androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW;
 import static androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -33,15 +33,15 @@
 
 import androidx.car.app.model.CarIcon;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Maneuver}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ManeuverTest {
 
     @Test
@@ -175,7 +175,7 @@
         assertThrows(
                 IllegalArgumentException.class,
                 () ->
-                        Maneuver.builder(TYPE_ROUNDABOUT_ENTER)
+                        Maneuver.builder(TYPE_ROUNDABOUT_ENTER_CW)
                                 .setRoundaboutExitNumber(1)
                                 .setRoundaboutExitAngle(1)
                                 .setIcon(CarIcon.APP_ICON)
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/MessageInfoTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/MessageInfoTest.java
index 1be5851..81063ca 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/MessageInfoTest.java
@@ -25,15 +25,15 @@
 
 import androidx.car.app.model.CarIcon;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link MessageInfoTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class MessageInfoTest {
 
     @Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
similarity index 86%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
index 7b82386..8e3b04a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
@@ -28,19 +28,18 @@
 import androidx.car.app.model.CarColor;
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.Distance;
-import androidx.car.app.utils.Logger;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.concurrent.TimeUnit;
 
 /** Tests for {@link NavigationTemplate}. */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class NavigationTemplateTest {
     private final ActionStrip mActionStrip =
             ActionStrip.builder().addAction(TestUtils.createAction("test", null)).build();
@@ -108,60 +107,6 @@
     }
 
     @Test
-    public void validate_isRefresh() {
-        Logger logger = message -> {
-        };
-
-        TravelEstimate travelEstimate =
-                TravelEstimate.create(
-                        Distance.create(/* displayDistance= */ 20, Distance.UNIT_METERS),
-                        TimeUnit.HOURS.toSeconds(1),
-                        createDateTimeWithZone("2020-05-14T19:57:00-07:00", "US/Pacific"));
-
-        Step currentStep =
-                Step.builder("Hop on a ferry")
-                        .addLane(
-                                Lane.builder()
-                                        .addDirection(LaneDirection.create(
-                                                LaneDirection.SHAPE_NORMAL_LEFT, false))
-                                        .build())
-                        .setLanesImage(CarIcon.ALERT)
-                        .build();
-        Distance currentDistance = Distance.create(/* displayDistance= */ 100,
-                Distance.UNIT_METERS);
-
-        NavigationTemplate reroutingTemplate =
-                NavigationTemplate.builder()
-                        .setNavigationInfo(RoutingInfo.builder().setIsLoading(true).build())
-                        .setActionStrip(mActionStrip)
-                        .build();
-
-        NavigationTemplate navigatingTemplate =
-                NavigationTemplate.builder()
-                        .setNavigationInfo(
-                                RoutingInfo.builder()
-                                        .setCurrentStep(currentStep, currentDistance)
-                                        .setJunctionImage(CarIcon.ALERT)
-                                        .setNextStep(currentStep)
-                                        .build())
-                        .setActionStrip(mActionStrip)
-                        .setDestinationTravelEstimate(travelEstimate)
-                        .setBackgroundColor(CarColor.BLUE)
-                        .build();
-
-        NavigationTemplate arrivedTemplate =
-                NavigationTemplate.builder()
-                        .setNavigationInfo(MessageInfo.builder("Arrived!").setText(
-                                "name\naddress").build())
-                        .setActionStrip(mActionStrip)
-                        .build();
-
-        assertThat(navigatingTemplate.isRefresh(reroutingTemplate, logger)).isTrue();
-        assertThat(arrivedTemplate.isRefresh(navigatingTemplate, logger)).isTrue();
-        assertThat(reroutingTemplate.isRefresh(arrivedTemplate, logger)).isTrue();
-    }
-
-    @Test
     public void equals() {
         TravelEstimate travelEstimate =
                 TravelEstimate.create(
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
similarity index 75%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
index ff808819..cd5aa98 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
@@ -37,19 +37,16 @@
 import androidx.car.app.model.PlaceMarker;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Toggle;
-import androidx.car.app.test.R;
-import androidx.car.app.utils.Logger;
-import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link PlaceListNavigationTemplate}. */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PlaceListNavigationTemplateTest {
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private final DistanceSpan mDistanceSpan =
@@ -63,7 +60,7 @@
                 () -> PlaceListNavigationTemplate.builder().setTitle("Title").build());
 
         // Positive case
-        PlaceListNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+        PlaceListNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
     }
 
     @Test
@@ -73,7 +70,7 @@
                 () ->
                         PlaceListNavigationTemplate.builder()
                                 .setTitle("Title")
-                                .setIsLoading(true)
+                                .setLoading(true)
                                 .setItemList(ItemList.builder().build())
                                 .build());
     }
@@ -283,116 +280,6 @@
     }
 
     @Test
-    public void validate_isRefresh() {
-        Logger logger = message -> {
-        };
-        SpannableString title = new SpannableString("Title");
-        title.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
-        Row.Builder row =
-                Row.builder().setTitle(title).setBrowsable(true).setOnClickListener(() -> {
-                });
-        PlaceListNavigationTemplate template =
-                PlaceListNavigationTemplate.builder()
-                        .setTitle("Title")
-                        .setItemList(ItemList.builder().addItem(row.build()).build())
-                        .build();
-
-        assertThat(template.isRefresh(template, logger)).isTrue();
-
-        // Going from loading state to new content is allowed.
-        assertThat(
-                template.isRefresh(
-                        PlaceListNavigationTemplate.builder().setTitle("Title").setIsLoading(
-                                true).build(),
-                        logger))
-                .isTrue();
-
-        // Other allowed mutable states.
-        SpannableString stringWithSpan = new SpannableString("Title");
-        stringWithSpan.setSpan(mDistanceSpan, 1, /* end= */ 2, /* flags= */ 0);
-        assertThat(template.isRefresh(PlaceListNavigationTemplate.builder()
-                        .setTitle("Title")
-                        .setItemList(ItemList.builder()
-                                .addItem(row.setOnClickListener(() -> {
-                                })
-                                        .setBrowsable(false)
-                                        .setTitle(stringWithSpan)
-                                        .setImage(CarIcon.of(
-                                                IconCompat.createWithResource(
-                                                        ApplicationProvider.getApplicationContext(),
-                                                        R.drawable.ic_test_1)))
-                                        .setMetadata(Metadata.ofPlace(
-                                                Place.builder(
-                                                        LatLng.create(
-                                                                1,
-                                                                1)).build()))
-                                        .build())
-                                .build())
-                        .setHeaderAction(Action.BACK)
-                        .setActionStrip(
-                                ActionStrip.builder().addAction(Action.APP_ICON).build())
-                        .build(),
-                logger))
-                .isTrue();
-
-        // Title updates are disallowed.
-        assertThat(
-                template.isRefresh(
-                        PlaceListNavigationTemplate.builder()
-                                .setItemList(ItemList.builder().addItem(row.build()).build())
-                                .setTitle("Title2")
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Text updates are disallowed.
-        SpannableString title2 = new SpannableString("Title2");
-        title2.setSpan(mDistanceSpan, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
-        assertThat(
-                template.isRefresh(
-                        PlaceListNavigationTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(ItemList.builder().addItem(
-                                        row.setTitle(title2).build()).build())
-                                .build(),
-                        logger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        PlaceListNavigationTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(
-                                        ItemList.builder()
-                                                .addItem(row.setTitle(title).addText(
-                                                        "Text").build())
-                                                .build())
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Additional rows are disallowed.
-        assertThat(
-                template.isRefresh(
-                        PlaceListNavigationTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(
-                                        ItemList.builder().addItem(row.build()).addItem(
-                                                row.build()).build())
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Going from content to loading state is disallowed.
-        assertThat(
-                PlaceListNavigationTemplate.builder()
-                        .setTitle("Title")
-                        .setIsLoading(true)
-                        .build()
-                        .isRefresh(template, logger))
-                .isFalse();
-    }
-
-    @Test
     public void equals() {
         PlaceListNavigationTemplate template =
                 PlaceListNavigationTemplate.builder()
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
similarity index 74%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
index d06b943..dcf4c11 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
@@ -26,8 +26,8 @@
 import android.text.SpannableString;
 
 import androidx.car.app.CarAppPermission;
+import androidx.car.app.OnDoneCallback;
 import androidx.car.app.TestUtils;
-import androidx.car.app.host.OnDoneCallback;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarIcon;
@@ -36,20 +36,16 @@
 import androidx.car.app.model.ItemList;
 import androidx.car.app.model.OnClickListener;
 import androidx.car.app.model.Row;
-import androidx.car.app.test.R;
-import androidx.car.app.utils.Logger;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link RoutePreviewNavigationTemplate}. */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class RoutePreviewNavigationTemplateTest {
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private static final DistanceSpan DISTANCE =
@@ -63,7 +59,7 @@
                 () -> RoutePreviewNavigationTemplate.builder().setTitle("Title").build());
 
         // Positive case
-        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
     }
 
     @Test
@@ -72,7 +68,7 @@
                 IllegalStateException.class,
                 () -> RoutePreviewNavigationTemplate.builder()
                         .setTitle("Title")
-                        .setIsLoading(true)
+                        .setLoading(true)
                         .setItemList(
                                 TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
                         .build());
@@ -109,7 +105,7 @@
                         .setItemList(
                                 ItemList.builder()
                                         .addItem(rowExceedsMaxTexts)
-                                        .setSelectable(selectedIndex -> {
+                                        .setOnSelectedListener(selectedIndex -> {
                                         })
                                         .build()));
 
@@ -119,7 +115,7 @@
                 .setItemList(
                         ItemList.builder()
                                 .addItem(rowMeetingMaxTexts)
-                                .setSelectable(selectedIndex -> {
+                                .setOnSelectedListener(selectedIndex -> {
                                 })
                                 .build());
     }
@@ -128,13 +124,13 @@
     public void noHeaderTitleOrAction_throws() {
         assertThrows(
                 IllegalStateException.class,
-                () -> RoutePreviewNavigationTemplate.builder().setIsLoading(true).build());
+                () -> RoutePreviewNavigationTemplate.builder().setLoading(true).build());
 
         // Positive cases.
-        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
         RoutePreviewNavigationTemplate.builder()
                 .setHeaderAction(Action.BACK)
-                .setIsLoading(true)
+                .setLoading(true)
                 .build();
     }
 
@@ -186,7 +182,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void setOnNavigateAction() {
         OnClickListener mockListener = mock(OnClickListener.class);
         RoutePreviewNavigationTemplate template =
@@ -217,7 +212,7 @@
                         .build());
 
         // Positive case
-        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
     }
 
     @Test
@@ -235,13 +230,13 @@
                         .build());
 
         // Positive case
-        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
     }
 
     @Test
     public void createInstance_navigateActionNoTitle_throws() {
-        CarIcon carIcon = CarIcon.of(IconCompat.createWithResource(
-                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                "ic_test_1");
         assertThrows(
                 IllegalArgumentException.class,
                 () -> RoutePreviewNavigationTemplate.builder()
@@ -272,9 +267,8 @@
         Row rowWithTime = Row.builder().setTitle(title).build();
         Row rowWithoutTime = Row.builder().setTitle("Google Bve").build();
         Action navigateAction = Action.builder()
-                .setIcon(CarIcon.of(IconCompat.createWithResource(
-                        ApplicationProvider.getApplicationContext(),
-                        R.drawable.ic_test_1)))
+                .setIcon(TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+                        "ic_test_1"))
                 .setTitle("Navigate")
                 .setOnClickListener(() -> {
                 })
@@ -287,7 +281,7 @@
                         .setItemList(ItemList.builder()
                                 .addItem(rowWithTime)
                                 .addItem(rowWithoutTime)
-                                .setSelectable(index -> {
+                                .setOnSelectedListener(index -> {
                                 })
                                 .build())
                         .setNavigateAction(navigateAction)
@@ -296,138 +290,13 @@
         // Positive case
         RoutePreviewNavigationTemplate.builder()
                 .setTitle("Title")
-                .setItemList(ItemList.builder().setSelectable(index -> {
+                .setItemList(ItemList.builder().setOnSelectedListener(index -> {
                 }).addItem(rowWithTime).build())
                 .setNavigateAction(navigateAction)
                 .build();
     }
 
     @Test
-    public void validate_isRefresh() {
-        Logger logger = message -> {
-        };
-        SpannableString title = new SpannableString("Title");
-        title.setSpan(DISTANCE, 0, 1, 0);
-        Row.Builder row = Row.builder().setTitle(title);
-        Action navigateAction = Action.builder().setTitle("Navigate").setOnClickListener(() -> {
-        }).build();
-        RoutePreviewNavigationTemplate template = RoutePreviewNavigationTemplate.builder()
-                .setTitle("Title")
-                .setItemList(ItemList.builder().addItem(row.build()).setSelectable(
-                        index -> {
-                        }).build())
-                .setNavigateAction(navigateAction)
-                .build();
-
-        assertThat(template.isRefresh(template, logger)).isTrue();
-
-        // Going from loading state to new content is allowed.
-        assertThat(template.isRefresh(
-                RoutePreviewNavigationTemplate.builder()
-                        .setTitle("Title")
-                        .setIsLoading(true)
-                        .build(),
-                logger))
-                .isTrue();
-
-        // Other allowed mutable states.
-        SpannableString stringWithSpan = new SpannableString("Title");
-        stringWithSpan.setSpan(DISTANCE, 1, /* end= */ 2, /* flags= */ 0);
-        assertThat(template.isRefresh(
-                RoutePreviewNavigationTemplate.builder()
-                        .setTitle("Title")
-                        .setItemList(ItemList.builder()
-                                .addItem(row.setImage(
-                                        CarIcon.of(IconCompat.createWithResource(
-                                                ApplicationProvider.getApplicationContext(),
-                                                R.drawable.ic_test_1)))
-                                        .setTitle(stringWithSpan)
-                                        .build())
-                                .setSelectable(index -> {
-                                })
-                                .build())
-                        .setHeaderAction(Action.BACK)
-                        .setNavigateAction(Action.builder().setTitle(
-                                "Navigate2").setOnClickListener(() -> {
-                                }
-                        ).build())
-                        .setActionStrip(ActionStrip.builder().addAction(Action.APP_ICON).build())
-                        .build(),
-                logger))
-                .isTrue();
-
-        // Title updates are disallowed.
-        assertThat(template.isRefresh(
-                RoutePreviewNavigationTemplate.builder()
-                        .setItemList(ItemList.builder().addItem(row.build()).setSelectable(
-                                index -> {
-                                }).build())
-                        .setTitle("Title2")
-                        .setNavigateAction(navigateAction)
-                        .build(),
-                logger))
-                .isFalse();
-
-        // Text updates are disallowed.
-        SpannableString title2 = new SpannableString("Title2");
-        title2.setSpan(DISTANCE, 0, 1, 0);
-        assertThat(
-                template.isRefresh(
-                        RoutePreviewNavigationTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(
-                                        ItemList.builder()
-                                                .addItem(row.setTitle(title2).build())
-                                                .setSelectable(index -> {
-                                                })
-                                                .build())
-                                .setNavigateAction(navigateAction)
-                                .build(),
-                        logger))
-                .isFalse();
-        assertThat(
-                template.isRefresh(
-                        RoutePreviewNavigationTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(
-                                        ItemList.builder()
-                                                .addItem(row.addText("Text").build())
-                                                .setSelectable(index -> {
-                                                })
-                                                .build())
-                                .setNavigateAction(navigateAction)
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Additional rows are disallowed.
-        assertThat(
-                template.isRefresh(
-                        RoutePreviewNavigationTemplate.builder()
-                                .setTitle("Title")
-                                .setItemList(
-                                        ItemList.builder()
-                                                .addItem(row.build())
-                                                .addItem(row.build())
-                                                .setSelectable(index -> {
-                                                })
-                                                .build())
-                                .setNavigateAction(navigateAction)
-                                .build(),
-                        logger))
-                .isFalse();
-
-        // Going from content to loading state is disallowed.
-        assertThat(
-                RoutePreviewNavigationTemplate.builder()
-                        .setTitle("Title")
-                        .setIsLoading(true)
-                        .build()
-                        .isRefresh(template, logger))
-                .isFalse();
-    }
-
-    @Test
     public void equals() {
         RoutePreviewNavigationTemplate template =
                 RoutePreviewNavigationTemplate.builder()
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutingInfoTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/RoutingInfoTest.java
index 58ea716..8cd974c 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutingInfoTest.java
@@ -26,15 +26,15 @@
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.Distance;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link RoutingInfoTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class RoutingInfoTest {
 
     private final Maneuver mManeuver =
@@ -54,7 +54,7 @@
         assertThrows(
                 IllegalStateException.class,
                 () -> RoutingInfo.builder()
-                        .setIsLoading(true)
+                        .setLoading(true)
                         .setCurrentStep(mCurrentStep, mCurrentDistance)
                         .build());
     }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/StepTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/StepTest.java
index bc8a79d..3c0f0c8 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/StepTest.java
@@ -24,15 +24,15 @@
 import static org.junit.Assert.assertThrows;
 
 import androidx.car.app.model.CarIcon;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 /** Tests for {@link Step}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class StepTest {
     @Test
     public void createInstance() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/TravelEstimateTest.java
similarity index 71%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/TravelEstimateTest.java
index 50d0c0c..ca589ec9 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/TravelEstimateTest.java
@@ -18,6 +18,7 @@
 
 import static androidx.car.app.TestUtils.assertDateTimeWithZoneEquals;
 import static androidx.car.app.TestUtils.createDateTimeWithZone;
+import static androidx.car.app.navigation.model.TravelEstimate.REMAINING_TIME_UNKNOWN;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -26,12 +27,11 @@
 import androidx.car.app.model.CarColor;
 import androidx.car.app.model.DateTimeWithZone;
 import androidx.car.app.model.Distance;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.time.Duration;
 import java.time.ZonedDateTime;
@@ -40,8 +40,8 @@
 import java.util.concurrent.TimeUnit;
 
 /** Tests for {@link TravelEstimate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class TravelEstimateTest {
     private final DateTimeWithZone mArrivalTime =
             createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
@@ -50,7 +50,21 @@
     private final long mRemainingTime = TimeUnit.HOURS.toMillis(10);
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
+    public void build_default_to_unknown_time() {
+        DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        long remainingTime = TimeUnit.HOURS.toMillis(10);
+        TravelEstimate travelEstimate =
+                TravelEstimate.builder(remainingDistance, arrivalTime).build();
+
+        assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+        assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(REMAINING_TIME_UNKNOWN);
+        assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+        assertThat(travelEstimate.getRemainingTimeColor()).isEqualTo(CarColor.DEFAULT);
+    }
+
+    @Test
     public void create_duration() {
         ZonedDateTime arrivalTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
         Duration remainingTime = Duration.ofHours(10);
@@ -82,6 +96,46 @@
     }
 
     @Test
+    public void create_unknown_remaining_time_in_seconds() {
+        DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(remainingDistance, REMAINING_TIME_UNKNOWN, arrivalTime);
+
+        assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+        assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(REMAINING_TIME_UNKNOWN);
+        assertThat(travelEstimate.getArrivalTimeAtDestination()).isEqualTo(arrivalTime);
+        assertThat(travelEstimate.getRemainingTimeColor()).isEqualTo(CarColor.DEFAULT);
+    }
+
+    @Test
+    public void create_unknown_remaining_time() {
+        ZonedDateTime arrivalTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        TravelEstimate travelEstimate =
+                TravelEstimate.create(
+                        remainingDistance, Duration.ofSeconds(REMAINING_TIME_UNKNOWN), arrivalTime);
+
+        assertThat(travelEstimate.getRemainingDistance()).isEqualTo(remainingDistance);
+        assertThat(travelEstimate.getRemainingTimeSeconds()).isEqualTo(REMAINING_TIME_UNKNOWN);
+        assertDateTimeWithZoneEquals(arrivalTime, travelEstimate.getArrivalTimeAtDestination());
+        assertThat(travelEstimate.getRemainingTimeColor()).isEqualTo(CarColor.DEFAULT);
+    }
+
+    @Test
+    public void create_invalid_remaining_time() {
+        DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
+        Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
+                Distance.UNIT_METERS);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> TravelEstimate.builder(remainingDistance,
+                        arrivalTime).setRemainingTimeSeconds(-2));
+    }
+
+    @Test
     public void create_custom_remainingTimeColor() {
         DateTimeWithZone arrivalTime = createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
         Distance remainingDistance = Distance.create(/* displayDistance= */ 100,
@@ -100,8 +154,8 @@
         for (CarColor carColor : allowedColors) {
             TravelEstimate travelEstimate =
                     TravelEstimate.builder(remainingDistance,
-                            TimeUnit.MILLISECONDS.toSeconds(remainingTime),
                             arrivalTime)
+                            .setRemainingTimeSeconds(TimeUnit.MILLISECONDS.toSeconds(remainingTime))
                             .setRemainingTimeColor(carColor)
                             .build();
 
@@ -132,8 +186,8 @@
         for (CarColor carColor : allowedColors) {
             TravelEstimate travelEstimate =
                     TravelEstimate.builder(remainingDistance,
-                            TimeUnit.MILLISECONDS.toSeconds(remainingTime),
                             arrivalTime)
+                            .setRemainingTimeSeconds(TimeUnit.MILLISECONDS.toSeconds(remainingTime))
                             .setRemainingDistanceColor(carColor)
                             .build();
 
@@ -155,8 +209,9 @@
                 IllegalArgumentException.class,
                 () ->
                         TravelEstimate.builder(remainingDistance,
-                                TimeUnit.MILLISECONDS.toSeconds(remainingTime),
                                 arrivalTime)
+                                .setRemainingTimeSeconds(
+                                        TimeUnit.MILLISECONDS.toSeconds(remainingTime))
                                 .setRemainingTimeColor(CarColor.createCustom(1, 2)));
     }
 
@@ -220,16 +275,17 @@
     public void notEquals_differentRemainingTimeColor() {
         TravelEstimate travelEstimate =
                 TravelEstimate.builder(mRemainingDistance,
-                        TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
                         mArrivalTime)
+                        .setRemainingTimeSeconds(TimeUnit.MILLISECONDS.toSeconds(mRemainingTime))
                         .setRemainingTimeColor(CarColor.YELLOW)
                         .build();
 
         assertThat(travelEstimate)
                 .isNotEqualTo(
                         TravelEstimate.builder(mRemainingDistance,
-                                TimeUnit.MILLISECONDS.toSeconds(mRemainingTime),
                                 mArrivalTime)
+                                .setRemainingTimeSeconds(
+                                        TimeUnit.MILLISECONDS.toSeconds(mRemainingTime))
                                 .setRemainingTimeColor(CarColor.GREEN)
                                 .build());
     }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/TripTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/TripTest.java
index 4c936e3..7fb0d3c 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/TripTest.java
@@ -26,17 +26,17 @@
 
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.Distance;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.concurrent.TimeUnit;
 
 /** Tests for {@link Trip}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class TripTest {
 
     private final Step mStep =
@@ -71,7 +71,7 @@
                         .addDestinationTravelEstimate(mDestinationTravelEstimate)
                         .addStepTravelEstimate(mStepTravelEstimate)
                         .setCurrentRoad(ROAD)
-                        .setIsLoading(false)
+                        .setLoading(false)
                         .build();
 
         assertThat(trip.getDestinations()).hasSize(1);
@@ -112,7 +112,7 @@
                         .addDestination(mDestination)
                         .addDestinationTravelEstimate(mDestinationTravelEstimate)
                         .setCurrentRoad(ROAD)
-                        .setIsLoading(true)
+                        .setLoading(true)
                         .build();
 
         assertThat(trip.getDestinations()).hasSize(1);
@@ -129,17 +129,17 @@
     public void createInstance_loading_with_steps() {
         assertThrows(
                 IllegalArgumentException.class,
-                () -> Trip.builder().addStep(mStep).setIsLoading(true).build());
+                () -> Trip.builder().addStep(mStep).setLoading(true).build());
         assertThrows(
                 IllegalArgumentException.class,
-                () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).setIsLoading(
+                () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).setLoading(
                         true).build());
         assertThrows(
                 IllegalArgumentException.class,
                 () -> Trip.builder()
                         .addStep(mStep)
                         .addStepTravelEstimate(mStepTravelEstimate)
-                        .setIsLoading(true)
+                        .setLoading(true)
                         .build());
     }
 }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java b/car/app/app/src/test/java/androidx/car/app/notification/CarAppExtenderTest.java
similarity index 83%
rename from car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
rename to car/app/app/src/test/java/androidx/car/app/notification/CarAppExtenderTest.java
index 1a35531..db35c5a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/notification/CarAppExtenderTest.java
@@ -27,22 +27,21 @@
 import android.os.Bundle;
 
 import androidx.annotation.NonNull;
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.List;
 
 /** Tests for {@link CarAppExtender}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public final class CarAppExtenderTest {
     private static final String NOTIFICATION_CHANNEL_ID = "test carextender channel id";
     private static final String INTENT_PRIMARY_ACTION =
@@ -76,8 +75,8 @@
         assertThat(carAppExtender.isExtended()).isFalse();
         assertThat(carAppExtender.getContentTitle()).isNull();
         assertThat(carAppExtender.getContentText()).isNull();
-        assertThat(carAppExtender.getSmallIconResId()).isEqualTo(0);
-        assertThat(carAppExtender.getLargeIconBitmap()).isNull();
+        assertThat(carAppExtender.getSmallIcon()).isEqualTo(0);
+        assertThat(carAppExtender.getLargeIcon()).isNull();
         assertThat(carAppExtender.getContentIntent()).isNull();
         assertThat(carAppExtender.getDeleteIntent()).isNull();
         assertThat(carAppExtender.getActions()).isEmpty();
@@ -129,22 +128,23 @@
 
     @Test
     public void notification_extended_setSmallIcon() {
-        int resId = R.drawable.ic_test_1;
+        int resId = TestUtils.getTestDrawableResId(mContext, "ic_test_1");
         NotificationCompat.Builder builder =
                 new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
                         .extend(CarAppExtender.builder().setSmallIcon(resId).build());
 
-        assertThat(new CarAppExtender(builder.build()).getSmallIconResId()).isEqualTo(resId);
+        assertThat(new CarAppExtender(builder.build()).getSmallIcon()).isEqualTo(resId);
     }
 
     @Test
     public void notification_extended_setLargeIcon() {
-        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_test_2);
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                TestUtils.getTestDrawableResId(mContext, "ic_test_2"));
         NotificationCompat.Builder builder =
                 new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
                         .extend(CarAppExtender.builder().setLargeIcon(bitmap).build());
 
-        assertThat(new CarAppExtender(builder.build()).getLargeIconBitmap()).isEqualTo(bitmap);
+        assertThat(new CarAppExtender(builder.build()).getLargeIcon()).isEqualTo(bitmap);
     }
 
     @Test
@@ -179,14 +179,13 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 23)
     public void notification_extended_addActions() {
-        int icon1 = R.drawable.ic_test_1;
+        int icon1 = TestUtils.getTestDrawableResId(mContext, "ic_test_1");
         CharSequence title1 = "FirstAction";
         Intent intent1 = new Intent(INTENT_PRIMARY_ACTION);
         PendingIntent actionIntent1 = PendingIntent.getBroadcast(mContext, 0, intent1, 0);
 
-        int icon2 = R.drawable.ic_test_2;
+        int icon2 = TestUtils.getTestDrawableResId(mContext, "ic_test_2");
         CharSequence title2 = "SecondAction";
         Intent intent2 = new Intent(INTENT_SECONDARY_ACTION);
         PendingIntent actionIntent2 = PendingIntent.getBroadcast(mContext, 0, intent2, 0);
@@ -210,31 +209,6 @@
     }
 
     @Test
-    public void notification_extended_clearActions() {
-        int icon1 = R.drawable.ic_test_1;
-        CharSequence title1 = "FirstAction";
-        Intent intent1 = new Intent(INTENT_PRIMARY_ACTION);
-        PendingIntent actionIntent1 = PendingIntent.getBroadcast(mContext, 0, intent1, 0);
-
-        int icon2 = R.drawable.ic_test_2;
-        CharSequence title2 = "SecondAction";
-        Intent intent2 = new Intent(INTENT_SECONDARY_ACTION);
-        PendingIntent actionIntent2 = PendingIntent.getBroadcast(mContext, 0, intent2, 0);
-
-        NotificationCompat.Builder builder =
-                new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
-                        .extend(
-                                CarAppExtender.builder()
-                                        .addAction(icon1, title1, actionIntent1)
-                                        .addAction(icon2, title2, actionIntent2)
-                                        .clearActions()
-                                        .build());
-
-        List<Action> actions = new CarAppExtender(builder.build()).getActions();
-        assertThat(actions).isEmpty();
-    }
-
-    @Test
     public void notification_extended_setImportance() {
         NotificationCompat.Builder builder =
                 new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/serialization/BundlerTest.java b/car/app/app/src/test/java/androidx/car/app/serialization/BundlerTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/serialization/BundlerTest.java
rename to car/app/app/src/test/java/androidx/car/app/serialization/BundlerTest.java
index eccc838..6bdf76b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/serialization/BundlerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/serialization/BundlerTest.java
@@ -31,7 +31,8 @@
 import android.os.RemoteException;
 
 import androidx.annotation.Nullable;
-import androidx.car.app.host.OnDoneCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.TestUtils;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.CarIcon;
@@ -46,17 +47,14 @@
 import androidx.car.app.model.Row;
 import androidx.car.app.serialization.Bundler.CycleDetectedBundlerException;
 import androidx.car.app.serialization.Bundler.TracedBundlerException;
-import androidx.car.app.test.R;
 import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
 
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.lang.reflect.Field;
 import java.util.ArrayList;
@@ -70,8 +68,8 @@
 import java.util.Set;
 
 /** Tests for {@link Bundler}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class BundlerTest {
     private static final String TAG_CLASS_NAME = "tag_class_name";
     private static final String TAG_CLASS_TYPE = "tag_class_type";
@@ -160,7 +158,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void binderSerialization() throws BundlerException, RemoteException {
         OnClickListener clickListener = mock(OnClickListener.class);
 
@@ -289,7 +286,8 @@
     @Test
     public void imageSerialization_resource() throws BundlerException {
         Context context = ApplicationProvider.getApplicationContext();
-        IconCompat image = IconCompat.createWithResource(context, R.drawable.ic_test_1);
+        IconCompat image = IconCompat.createWithResource(context,
+                TestUtils.getTestDrawableResId(mContext, "ic_test_1"));
         Bundle bundle = Bundler.toBundle(image);
 
         assertThat(CarIcon.of((IconCompat) Bundler.fromBundle(bundle))).isEqualTo(
@@ -307,7 +305,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 23)
     public void imageSerialization_Icon() throws BundlerException {
         Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888);
         try {
@@ -332,10 +329,7 @@
         String row1Subtitle = "row1subtitle";
 
         LatLng latLng2 = LatLng.create(4522.234, 34.234);
-        CarIcon carIcon =
-                CarIcon.of(
-                        IconCompat.createWithResource(
-                                ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+        CarIcon carIcon = TestUtils.getTestCarIcon(mContext, "ic_test_1");
         PlaceMarker marker2 = PlaceMarker.builder().setIcon(carIcon, PlaceMarker.TYPE_ICON).build();
         String row2Title = "row2";
         String row2Subtitle = "row2subtitle";
@@ -530,12 +524,14 @@
         assertThrows(BundlerException.class, () -> Bundler.fromBundle(bundle));
     }
 
-    @Test
-    public void classMissingDefaultConstructorSerialization_throwsBundlerException() {
-        assertThrows(
-                BundlerException.class,
-                () -> Bundler.toBundle(new TestClassMissingDefaultConstructor(1)));
-    }
+    //TODO(rampara): Investigate why default constructor is still found when running with
+    // robolectric.
+//    @Test
+//    public void classMissingDefaultConstructorSerialization_throwsBundlerException() {
+//        assertThrows(
+//                BundlerException.class,
+//                () -> Bundler.toBundle(new TestClassMissingDefaultConstructor(1)));
+//    }
 
     @Test
     public void arraySerialization_throwsBundlerException() {
@@ -551,8 +547,7 @@
 
     @Test
     public void imageCompat_dejetify() throws BundlerException {
-        CarIcon image = CarIcon.of(IconCompat.createWithResource(mContext, R.drawable.ic_test_1));
-
+        CarIcon image = TestUtils.getTestCarIcon(mContext, "ic_test_1");
         Bundle bundle = Bundler.toBundle(image);
 
         // Get the field for the image, and re-write it with a "jetified" key.
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/CarAppServiceController.java b/car/app/app/src/test/java/androidx/car/app/testing/CarAppServiceController.java
similarity index 75%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/CarAppServiceController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/CarAppServiceController.java
index 709f742..b88a00b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/CarAppServiceController.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/CarAppServiceController.java
@@ -24,9 +24,11 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.car.app.AppInfo;
 import androidx.car.app.CarAppService;
 import androidx.car.app.HostInfo;
 import androidx.car.app.ICarApp;
+import androidx.car.app.Session;
 import androidx.lifecycle.Lifecycle.State;
 
 import java.lang.reflect.Field;
@@ -39,7 +41,7 @@
  *
  * <ul>
  *   <li>Sending different {@link Intent}s to the {@link CarAppService}'s {@link
- *       CarAppService#onCreateScreen} and {@link CarAppService#onNewIntent} methods.
+ *       Session#onCreateScreen} and {@link CarAppService#onNewIntent} methods.
  *   <li>Moving a {@link CarAppService} through its different {@link State}s.
  * </ul>
  */
@@ -50,15 +52,18 @@
 
     /** Creates a {@link CarAppServiceController} to control the provided {@link CarAppService}. */
     public static CarAppServiceController of(
-            @NonNull TestCarContext testCarContext, @NonNull CarAppService carAppService) {
+            @NonNull TestCarContext testCarContext,
+            @NonNull Session session, @NonNull CarAppService carAppService) {
         return new CarAppServiceController(
-                requireNonNull(carAppService), requireNonNull(testCarContext));
+                requireNonNull(carAppService), requireNonNull(session),
+                requireNonNull(testCarContext));
     }
 
     /**
      * Initializes the {@link CarAppService} that is being controlled.
      *
-     * <p>This will send an empty {@link Intent} to {@link CarAppService#onCreateScreen}.
+     * <p>This will send an empty {@link Intent} to the {@link Session} returned from
+     * {@link CarAppService#onCreateSession}.
      */
     public CarAppServiceController create() {
         return create(
@@ -69,7 +74,7 @@
     /**
      * Initializes the {@link CarAppService} that is being controlled.
      *
-     * <p>This will send the provided {@link Intent} to {@link CarAppService#onCreateScreen}.
+     * <p>This will send the provided {@link Intent} to {@link Session#onCreateScreen}.
      */
     public CarAppServiceController create(@NonNull Intent intent) {
         Objects.requireNonNull(intent);
@@ -103,7 +108,7 @@
     /**
      * Starts the {@link CarAppService} that is being controlled.
      *
-     * @see CarAppService#getLifecycle
+     * @see Session#getLifecycle
      */
     public CarAppServiceController start() {
         try {
@@ -118,7 +123,7 @@
     /**
      * Resumes the {@link CarAppService} that is being controlled.
      *
-     * @see CarAppService#getLifecycle
+     * @see Session#getLifecycle
      */
     public CarAppServiceController resume() {
         try {
@@ -133,7 +138,7 @@
     /**
      * Pauses the {@link CarAppService} that is being controlled.
      *
-     * @see CarAppService#getLifecycle
+     * @see Session#getLifecycle
      */
     public CarAppServiceController pause() {
         try {
@@ -148,7 +153,7 @@
     /**
      * Stops the {@link CarAppService} that is being controlled.
      *
-     * @see CarAppService#getLifecycle
+     * @see Session#getLifecycle
      */
     public CarAppServiceController stop() {
         try {
@@ -162,11 +167,10 @@
     /**
      * Destroys the {@link CarAppService} that is being controlled.
      *
-     * @see CarAppService#getLifecycle
+     * @see Session#getLifecycle
      */
     public CarAppServiceController destroy() {
         mCarAppService.onUnbind(new Intent());
-        mCarAppService.onCarAppFinished();
         mCarAppService.onDestroy();
         return this;
     }
@@ -182,6 +186,17 @@
         }
     }
 
+    public void setAppInfo(@Nullable AppInfo appInfo) {
+        try {
+            Field appInfoField = CarAppService.class.getDeclaredField("mAppInfo");
+            appInfoField.setAccessible(true);
+            appInfoField.set(mCarAppService, appInfo);
+        } catch (ReflectiveOperationException e) {
+            throw new IllegalStateException(
+                    "Failed to set CarAppService appInfo value for testing", e);
+        }
+    }
+
     /** Retrieves the {@link CarAppService} that is being controlled. */
     @NonNull
     public CarAppService get() {
@@ -189,19 +204,24 @@
     }
 
     private CarAppServiceController(
-            CarAppService carAppService, @NonNull TestCarContext testCarContext) {
+            CarAppService carAppService,
+            @NonNull Session session, @NonNull TestCarContext testCarContext) {
         this.mCarAppService = carAppService;
         this.mTestCarContext = testCarContext;
 
-        // Use reflection to inject the TestCarContext into the Screen.
+        // Use reflection to inject the Session and TestCarContext into the CarAppService.
         try {
-            Field registry = CarAppService.class.getDeclaredField("mRegistry");
-            registry.setAccessible(true);
-            registry.set(carAppService, testCarContext.getLifecycleOwner().mRegistry);
+            Field currentSession = CarAppService.class.getDeclaredField("mCurrentSession");
+            currentSession.setAccessible(true);
+            currentSession.set(carAppService, session);
 
-            Field carContext = CarAppService.class.getDeclaredField("mCarContext");
+            Field registry = Session.class.getDeclaredField("mRegistry");
+            registry.setAccessible(true);
+            registry.set(session, testCarContext.getLifecycleOwner().mRegistry);
+
+            Field carContext = Session.class.getDeclaredField("mCarContext");
             carContext.setAccessible(true);
-            carContext.set(carAppService, testCarContext);
+            carContext.set(session, testCarContext);
         } catch (ReflectiveOperationException e) {
             throw new IllegalStateException(
                     "Failed to set internal CarAppService values for testing", e);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/FakeHost.java b/car/app/app/src/test/java/androidx/car/app/testing/FakeHost.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/FakeHost.java
rename to car/app/app/src/test/java/androidx/car/app/testing/FakeHost.java
index e6e30b1..498870a5 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/FakeHost.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/FakeHost.java
@@ -41,8 +41,8 @@
  * <p>This fake allows sending a {@link PendingIntent} as if the user clicked on a notification
  * action.
  *
- * <p>It will also perform expected host behaviors such as calling {@link Screen#getTemplate} after
- * {@link AppManager#invalidate} is called.
+ * <p>It will also perform expected host behaviors such as calling {@link Screen#onGetTemplate}
+ * after {@link AppManager#invalidate} is called.
  */
 public class FakeHost {
     private final ICarHost.Stub mCarHost = new TestCarHost();
@@ -73,7 +73,7 @@
 
         Bundle extras = new Bundle(1);
         extras.putBinder(
-                CarContext.START_CAR_APP_BINDER_KEY,
+                CarContext.EXTRA_START_CAR_APP_BINDER_KEY,
                 mTestCarContext.getStartCarAppStub().asBinder());
         Intent extraData = new Intent().putExtras(extras);
 
@@ -122,7 +122,7 @@
             Screen top = mTestCarContext.getCarService(TestScreenManager.class).getTop();
             mTestCarContext
                     .getCarService(TestAppManager.class)
-                    .addTemplateReturned(top, top.getTemplate());
+                    .addTemplateReturned(top, top.onGetTemplate());
         }
 
         @Override
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/ScreenController.java b/car/app/app/src/test/java/androidx/car/app/testing/ScreenController.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/ScreenController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/ScreenController.java
index 07cb534..8565154 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/ScreenController.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/ScreenController.java
@@ -38,7 +38,7 @@
  *
  * <ul>
  *   <li>Moving a {@link Screen} through its different {@link State}s.
- *   <li>Retrieving all {@link Template}s returned from {@link Screen#getTemplate}. The values can
+ *   <li>Retrieving all {@link Template}s returned from {@link Screen#onGetTemplate}. The values can
  *       be reset with {@link #reset}.
  * </ul>
  */
@@ -58,7 +58,7 @@
     }
 
     /**
-     * Retrieves all the {@link Template}s returned from {@link Screen#getTemplate} for the {@link
+     * Retrieves all the {@link Template}s returned from {@link Screen#onGetTemplate} for the {@link
      * Screen} being controlled.
      *
      * <p>The templates are stored in order of calls.
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestAppManager.java b/car/app/app/src/test/java/androidx/car/app/testing/TestAppManager.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestAppManager.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestAppManager.java
index 92d74b5..9a5b1f0 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestAppManager.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/TestAppManager.java
@@ -36,8 +36,8 @@
  *
  * <ul>
  *   <li>All {@link SurfaceListener}s set via calling {@link AppManager#setSurfaceListener}.
- *   <li>The {@link Template}s returned from {@link Screen#getTemplate} due to invalidate calls via
- *       {@link AppManager#invalidate}.
+ *   <li>The {@link Template}s returned from {@link Screen#onGetTemplate} due to invalidate calls
+ *       via {@link AppManager#invalidate}.
  *   <li>All toasts shown via calling {@link AppManager#showToast}.
  * </ul>
  */
@@ -79,8 +79,9 @@
     }
 
     /**
-     * Retrieves all the {@link Template}s returned from {@link Screen#getTemplate} due to a call to
-     * {@link AppManager#invalidate}, and the respective {@link Screen} instance that returned it.
+     * Retrieves all the {@link Template}s returned from {@link Screen#onGetTemplate} due to a call
+     * to {@link AppManager#invalidate}, and the respective {@link Screen} instance that returned
+     * it.
      *
      * <p>The results are stored in order of calls.
      *
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestCarContext.java b/car/app/app/src/test/java/androidx/car/app/testing/TestCarContext.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestCarContext.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestCarContext.java
index 4f09fbd..19e5091 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestCarContext.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/TestCarContext.java
@@ -86,7 +86,7 @@
         } else if (serviceClass.isInstance(mTestNavigationManager)) {
             serviceName = NAVIGATION_SERVICE;
         } else if (serviceClass.isInstance(mTestScreenManager)) {
-            serviceName = SCREEN_MANAGER_SERVICE;
+            serviceName = SCREEN_SERVICE;
         } else {
             serviceName = getCarServiceName(serviceClass);
         }
@@ -107,7 +107,7 @@
                 return mTestAppManager;
             case CarContext.NAVIGATION_SERVICE:
                 return mTestNavigationManager;
-            case CarContext.SCREEN_MANAGER_SERVICE:
+            case CarContext.SCREEN_SERVICE:
                 return mTestScreenManager;
             default:
                 // Fall out
@@ -226,7 +226,7 @@
         this.mFakeHost = new FakeHost(this);
         this.mTestLifecycleOwner = testLifecycleOwner;
         this.mTestAppManager = new TestAppManager(this, hostDispatcher);
-        this.mTestNavigationManager = new TestNavigationManager(hostDispatcher);
+        this.mTestNavigationManager = new TestNavigationManager(this, hostDispatcher);
         this.mTestScreenManager = new TestScreenManager(this);
     }
 }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestLifecycleOwner.java b/car/app/app/src/test/java/androidx/car/app/testing/TestLifecycleOwner.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestLifecycleOwner.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestLifecycleOwner.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestOnDoneCallbackStub.java b/car/app/app/src/test/java/androidx/car/app/testing/TestOnDoneCallbackStub.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestOnDoneCallbackStub.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestOnDoneCallbackStub.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestScreenManager.java b/car/app/app/src/test/java/androidx/car/app/testing/TestScreenManager.java
similarity index 94%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestScreenManager.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestScreenManager.java
index 89e1445..85f375e 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestScreenManager.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/TestScreenManager.java
@@ -17,7 +17,7 @@
 package androidx.car.app.testing;
 
 import androidx.annotation.NonNull;
-import androidx.car.app.OnScreenResultCallback;
+import androidx.car.app.OnScreenResultListener;
 import androidx.car.app.Screen;
 import androidx.car.app.ScreenManager;
 import androidx.lifecycle.Lifecycle.State;
@@ -92,9 +92,9 @@
 
     @Override
     public void pushForResult(
-            @NonNull Screen screen, @NonNull OnScreenResultCallback onScreenResultCallback) {
+            @NonNull Screen screen, @NonNull OnScreenResultListener onScreenResultListener) {
         mScreensPushed.add(screen);
-        super.pushForResult(screen, onScreenResultCallback);
+        super.pushForResult(screen, onScreenResultListener);
     }
 
     @Override
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/model/ControllerUtil.java b/car/app/app/src/test/java/androidx/car/app/testing/model/ControllerUtil.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/model/ControllerUtil.java
rename to car/app/app/src/test/java/androidx/car/app/testing/model/ControllerUtil.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/model/LaneController.java b/car/app/app/src/test/java/androidx/car/app/testing/model/LaneController.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/model/LaneController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/model/LaneController.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/TestNavigationManager.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/TestNavigationManager.java
similarity index 85%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/TestNavigationManager.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/TestNavigationManager.java
index db99643..3892e58 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/TestNavigationManager.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/navigation/TestNavigationManager.java
@@ -24,6 +24,7 @@
 import androidx.car.app.navigation.NavigationManager;
 import androidx.car.app.navigation.NavigationManagerListener;
 import androidx.car.app.navigation.model.Trip;
+import androidx.car.app.testing.TestCarContext;
 import androidx.car.app.testing.navigation.model.TripController;
 
 import java.util.ArrayList;
@@ -37,7 +38,8 @@
  *
  * <ul>
  *   <li>All the {@link Trip}s sent via {@link NavigationManager#updateTrip}.
- *   <li>All the {@link NavigationManagerListener}s set via {@link NavigationManager#setListener}.
+ *   <li>All the {@link NavigationManagerListener}s set via
+ *   {@link NavigationManager#setNavigationManagerListener}.
  *   <li>Count of times that the navigation was started via {@link
  *       NavigationManager#navigationStarted()}.
  *   <li>Count of times that the navigation was ended via {@link NavigationManager#navigationEnded}.
@@ -71,14 +73,14 @@
 
     /**
      * Retrieves all the {@link NavigationManagerListener}s added via {@link
-     * NavigationManager#setListener(NavigationManagerListener)}.
+     * NavigationManager#setNavigationManagerListener(NavigationManagerListener)}.
      *
      * <p>The listeners are stored in order of calls.
      *
      * <p>The listeners will be stored until {@link #reset} is called.
      */
     @NonNull
-    public List<NavigationManagerListener> getNavigationManagerListenersSet() {
+    public List<NavigationManagerListener> getNavigationManagerCallbacksSet() {
         return mListenersSet;
     }
 
@@ -105,9 +107,9 @@
     }
 
     @Override
-    public void setListener(@Nullable NavigationManagerListener listener) {
+    public void setNavigationManagerListener(@Nullable NavigationManagerListener listener) {
         mListenersSet.add(listener);
-        super.setListener(listener);
+        super.setNavigationManagerListener(listener);
     }
 
     @Override
@@ -122,7 +124,7 @@
         super.navigationEnded();
     }
 
-    public TestNavigationManager(HostDispatcher hostDispatcher) {
-        super(hostDispatcher);
+    public TestNavigationManager(TestCarContext testCarContext, HostDispatcher hostDispatcher) {
+        super(testCarContext, hostDispatcher);
     }
 }
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/DestinationController.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/DestinationController.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/DestinationController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/model/DestinationController.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/StepController.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/StepController.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/StepController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/model/StepController.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/TripController.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/TripController.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/TripController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/model/TripController.java
index c6e9e10..692d408 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/TripController.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/TripController.java
@@ -40,7 +40,7 @@
  *   <li>The {@link TravelEstimate}s set via {@link Trip.Builder#addDestinationTravelEstimate}.
  *   <li>The {@link TravelEstimate}s set via {@link Trip.Builder#addStepTravelEstimate}.
  *   <li>The current road set via {@link Trip.Builder#setCurrentRoad}.
- *   <li>The loading state set via {@link Trip.Builder#setIsLoading}.
+ *   <li>The loading state set via {@link Trip.Builder#setLoading}.
  * </ul>
  */
 public class TripController {
diff --git a/car/app/app/src/test/java/androidx/car/app/versioning/CarAppApiLevelsTest.java b/car/app/app/src/test/java/androidx/car/app/versioning/CarAppApiLevelsTest.java
new file mode 100644
index 0000000..916e933
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/versioning/CarAppApiLevelsTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.versioning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class CarAppApiLevelsTest {
+    @Test
+    public void isValid_apiLowerThanOldest_notValid() {
+        assertThat(CarAppApiLevels.isValid(CarAppApiLevels.OLDEST - 1)).isFalse();
+    }
+
+    @Test
+    public void isValid_apiHigherThanLatest_notValid() {
+        assertThat(CarAppApiLevels.isValid(CarAppApiLevels.LATEST + 1)).isFalse();
+    }
+}
diff --git a/car/app/app/src/androidTest/res/drawable-hdpi/banana.png b/car/app/app/src/test/res/drawable-hdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-hdpi/banana.png
rename to car/app/app/src/test/res/drawable-hdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-ldpi/banana.png b/car/app/app/src/test/res/drawable-ldpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-ldpi/banana.png
rename to car/app/app/src/test/res/drawable-ldpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-mdpi/banana.png b/car/app/app/src/test/res/drawable-mdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-mdpi/banana.png
rename to car/app/app/src/test/res/drawable-mdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-xhdpi/banana.png b/car/app/app/src/test/res/drawable-xhdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-xhdpi/banana.png
rename to car/app/app/src/test/res/drawable-xhdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-xxhdpi/banana.png b/car/app/app/src/test/res/drawable-xxhdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-xxhdpi/banana.png
rename to car/app/app/src/test/res/drawable-xxhdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable/banana.png b/car/app/app/src/test/res/drawable/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable/banana.png
rename to car/app/app/src/test/res/drawable/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_1.xml b/car/app/app/src/test/res/drawable/ic_test_1.xml
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable/ic_test_1.xml
rename to car/app/app/src/test/res/drawable/ic_test_1.xml
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_2.xml b/car/app/app/src/test/res/drawable/ic_test_2.xml
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable/ic_test_2.xml
rename to car/app/app/src/test/res/drawable/ic_test_2.xml
diff --git a/car/app/app/src/test/resources/robolectric.properties b/car/app/app/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..ce87047
--- /dev/null
+++ b/car/app/app/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
+# sdk for now. Remove when no longer necessary.
+sdk=29
diff --git a/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.java b/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.java
new file mode 100644
index 0000000..feafa5c
--- /dev/null
+++ b/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class IndexBasedArrayIteratorTest {
+
+    @Test
+    public void iterateAll() {
+        Iterator<String> iterator = new ArraySet<>(setOf("a", "b", "c")).iterator();
+        assertThat(toList(iterator)).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void iterateEmptyList() {
+        Iterator<String> iterator = new ArraySet<String>().iterator();
+        assertThat(iterator.hasNext()).isFalse();
+    }
+
+    @Test(expected = NoSuchElementException.class)
+    public void iterateEmptyListThrowsUponNext() {
+        Iterator<String> iterator = new ArraySet<String>().iterator();
+        iterator.next();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void removeSameItemTwice() {
+        Iterator<String> iterator = new ArraySet<>(listOf("a", "b", "c")).iterator();
+        iterator.next(); // move to next
+        iterator.remove();
+        iterator.remove();
+    }
+
+
+    @Test
+    public void removeLast() {
+        removeViaIterator(
+                /* original= */ setOf("a", "b", "c"),
+                /* toBeRemoved= */ setOf("c"),
+                /* expected= */ setOf("a", "b"));
+    }
+
+    @Test
+    public void removeFirst() {
+        removeViaIterator(
+                /* original= */ setOf("a", "b", "c"),
+                /* toBeRemoved= */ setOf("a"),
+                /* expected= */ setOf("b", "c"));
+    }
+
+    @Test
+    public void removeMid() {
+        removeViaIterator(
+                /* original= */ setOf("a", "b", "c"),
+                /* toBeRemoved= */ setOf("b"),
+                /* expected= */ setOf("a", "c"));
+    }
+
+    @Test
+    public void removeConsecutive() {
+        removeViaIterator(
+                /* original= */ setOf("a", "b", "c", "d"),
+                /* toBeRemoved= */ setOf("b", "c"),
+                /* expected= */ setOf("a", "d"));
+    }
+
+    @Test
+    public void removeLastTwo() {
+        removeViaIterator(
+                /* original= */ setOf("a", "b", "c", "d"),
+                /* toBeRemoved= */ setOf("c", "d"),
+                /* expected= */ setOf("a", "b"));
+    }
+
+    @Test
+    public void removeFirstTwo() {
+        removeViaIterator(
+                /* original= */ setOf("a", "b", "c", "d"),
+                /* toBeRemoved= */ setOf("a", "b"),
+                /* expected= */ setOf("c", "d"));
+    }
+
+    @Test
+    public void removeMultiple() {
+        removeViaIterator(
+                /* original= */ setOf("a", "b", "c", "d"),
+                /* toBeRemoved= */ setOf("a", "c"),
+                /* expected= */ setOf("b", "d"));
+    }
+
+    private static void removeViaIterator(
+            Set<String> original,
+            Set<String> toBeRemoved,
+            Set<String> expected) {
+        ArraySet<String> subject = new ArraySet<>(original);
+        Iterator<String> iterator = subject.iterator();
+        while (iterator.hasNext()) {
+            String next = iterator.next();
+            if (toBeRemoved.contains(next)) {
+                iterator.remove();
+            }
+        }
+        assertThat(subject).containsExactlyElementsIn(expected);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <V> List<V> listOf(V... values) {
+        List<V> list = new ArrayList<>();
+        for (V value : values) {
+            list.add(value);
+        }
+        return list;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <V> Set<V> setOf(V... values) {
+        Set<V> set = new HashSet<>();
+        for (V value : values) {
+            set.add(value);
+        }
+        return set;
+    }
+
+    private static <V> List<V> toList(Iterator<V> iterator) {
+        List<V> list = new ArrayList<>();
+        while (iterator.hasNext()) {
+            list.add(iterator.next());
+        }
+        return list;
+    }
+}
diff --git a/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.kt b/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.kt
deleted file mode 100644
index e9326f2..0000000
--- a/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.collection
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class IndexBasedArrayIteratorTest {
-
-    @Test
-    fun iterateAll() {
-        val iterator = ArraySet(setOf("a", "b", "c")).iterator()
-        assertThat(iterator.asSequence().toList()).containsExactly("a", "b", "c")
-    }
-
-    @Test
-    fun iterateEmptyList() {
-        val iterator = ArraySet<String>().iterator()
-        assertThat(iterator.hasNext()).isFalse()
-
-        assertThat(
-            runCatching {
-                iterator.next()
-            }.exceptionOrNull()
-        ).isInstanceOf(NoSuchElementException::class.java)
-    }
-
-    @Test
-    fun removeSameItemTwice() {
-        val iterator = ArraySet(listOf("a", "b", "c")).iterator()
-        iterator.next() // move to next
-        iterator.remove()
-        assertThat(
-            runCatching {
-                iterator.remove()
-            }.exceptionOrNull()
-        ).isInstanceOf(IllegalStateException::class.java)
-    }
-
-    @Test
-    fun removeLast() = removeViaIterator(
-        original = setOf("a", "b", "c"),
-        toBeRemoved = setOf("c"),
-        expected = setOf("a", "b")
-    )
-
-    @Test
-    fun removeFirst() = removeViaIterator(
-        original = setOf("a", "b", "c"),
-        toBeRemoved = setOf("a"),
-        expected = setOf("b", "c")
-    )
-
-    @Test
-    fun removeMid() = removeViaIterator(
-        original = setOf("a", "b", "c"),
-        toBeRemoved = setOf("b"),
-        expected = setOf("a", "c")
-    )
-
-    @Test
-    fun removeConsecutive() = removeViaIterator(
-        original = setOf("a", "b", "c", "d"),
-        toBeRemoved = setOf("b", "c"),
-        expected = setOf("a", "d")
-    )
-
-    @Test
-    fun removeLastTwo() = removeViaIterator(
-        original = setOf("a", "b", "c", "d"),
-        toBeRemoved = setOf("c", "d"),
-        expected = setOf("a", "b")
-    )
-
-    @Test
-    fun removeFirstTwo() = removeViaIterator(
-        original = setOf("a", "b", "c", "d"),
-        toBeRemoved = setOf("a", "b"),
-        expected = setOf("c", "d")
-    )
-
-    @Test
-    fun removeMultiple() = removeViaIterator(
-        original = setOf("a", "b", "c", "d"),
-        toBeRemoved = setOf("a", "c"),
-        expected = setOf("b", "d")
-    )
-
-    private fun removeViaIterator(
-        original: Set<String>,
-        toBeRemoved: Set<String>,
-        expected: Set<String>
-    ) {
-        val subject = ArraySet(original)
-        val iterator = subject.iterator()
-        while (iterator.hasNext()) {
-            val next = iterator.next()
-            if (next in toBeRemoved) {
-                iterator.remove()
-            }
-        }
-        assertThat(subject).containsExactlyElementsIn(expected)
-    }
-}
diff --git a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt
index 223f3c9..5a44235 100644
--- a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt
+++ b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt
@@ -30,7 +30,7 @@
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.integration.demos.common.DemoCategory
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
@@ -120,7 +120,7 @@
         }
     Button(
         onClick = { state.value = !state.value },
-        colors = ButtonConstants.defaultButtonColors(backgroundColor = color)
+        colors = ButtonDefaults.buttonColors(backgroundColor = color)
     ) {
         Text("Click me")
     }
diff --git a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt
index ca16cf3..66b18e22 100644
--- a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt
+++ b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt
@@ -34,12 +34,10 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 
-@OptIn(ExperimentalFocus::class)
 @Composable
 fun EditTextInteropDemo() {
     Column {
diff --git a/compose/animation/animation-core/api/api_lint.ignore b/compose/animation/animation-core/api/api_lint.ignore
index 8682399..1c23773 100644
--- a/compose/animation/animation-core/api/api_lint.ignore
+++ b/compose/animation/animation-core/api/api_lint.ignore
@@ -1,10 +1,4 @@
 // Baseline format: 1.0
-ArrayReturn: androidx.compose.animation.core.TransitionDefinition#snapTransition(kotlin.Pair<? extends T,? extends T>[], T) parameter #0:
-    Method parameter should be Collection<Pair> (or subclass) instead of raw array; was `kotlin.Pair<? extends T,? extends T>[]`
-ArrayReturn: androidx.compose.animation.core.TransitionDefinition#transition(kotlin.Pair<? extends T,? extends T>[], kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionSpec<T>,kotlin.Unit>) parameter #0:
-    Method parameter should be Collection<Pair> (or subclass) instead of raw array; was `kotlin.Pair<? extends T,? extends T>[]`
-
-
 AutoBoxing: androidx.compose.animation.core.CubicBezierEasing#invoke(float):
     Must avoid boxed primitives (`java.lang.Float`)
 AutoBoxing: androidx.compose.animation.core.DecayAnimation#getTargetValue():
diff --git a/compose/animation/animation-core/api/current.txt b/compose/animation/animation-core/api/current.txt
index 2603144..41d1e7f 100644
--- a/compose/animation/animation-core/api/current.txt
+++ b/compose/animation/animation-core/api/current.txt
@@ -55,7 +55,7 @@
   public final class AnimationConstants {
     field public static final int DefaultDurationMillis = 300; // 0x12c
     field public static final androidx.compose.animation.core.AnimationConstants INSTANCE;
-    field public static final int Infinite = 2147483647; // 0x7fffffff
+    field @Deprecated public static final int Infinite = 2147483647; // 0x7fffffff
   }
 
   public enum AnimationEndReason {
@@ -115,6 +115,7 @@
     method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> AnimationState-SZyJPdc(float initialValue, optional float initialVelocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
     method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> copy-mN48zRs(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>, optional float value, optional float velocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
     method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.AnimationState<T,V> copy-upwry94(androidx.compose.animation.core.AnimationState<T,V>, optional T? value, optional V? velocityVector, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
+    method public static <T, V extends androidx.compose.animation.core.AnimationVector> V createZeroVectorFrom(androidx.compose.animation.core.TwoWayConverter<T,V>, T? value);
     method public static float getVelocity(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
     method public static float getVelocity(androidx.compose.animation.core.AnimationScope<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
     method public static boolean isFinished(androidx.compose.animation.core.AnimationState<?,?>);
@@ -235,7 +236,7 @@
     method public void dispatchTime$metalava_module(long frameTimeMillis);
   }
 
-  public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+  public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.FiniteAnimationSpec<T> {
     method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
   }
 
@@ -257,6 +258,10 @@
     property public float absVelocityThreshold;
   }
 
+  public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+  }
+
   public interface FloatAnimationSpec extends androidx.compose.animation.core.AnimationSpec<java.lang.Float> {
     method public long getDurationMillis(float start, float end, float startVelocity);
     method public default float getEndVelocity(float start, float end, float startVelocity);
@@ -309,6 +314,15 @@
     property public final int duration;
   }
 
+  public final class InfiniteRepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+    ctor public InfiniteRepeatableSpec(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+    method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
+    method public androidx.compose.animation.core.RepeatMode getRepeatMode();
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+    property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
+    property public final androidx.compose.animation.core.RepeatMode repeatMode;
+  }
+
   public final class IntPropKey implements androidx.compose.animation.core.PropKey<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> {
     ctor public IntPropKey(String label);
     ctor public IntPropKey();
@@ -394,8 +408,6 @@
 
   public final class PropKeyKt {
     method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.TwoWayConverter<T,V> TwoWayConverter(kotlin.jvm.functions.Function1<? super T,? extends V> convertToVector, kotlin.jvm.functions.Function1<? super V,? extends T> convertFromVector);
-    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getFloatToVectorConverter();
-    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getIntToVectorConverter();
     method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.FloatCompanionObject);
     method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.IntCompanionObject);
   }
@@ -405,18 +417,18 @@
     enum_constant public static final androidx.compose.animation.core.RepeatMode Reverse;
   }
 
-  @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public RepeatableSpec(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
     method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
     method public int getIterations();
     method public androidx.compose.animation.core.RepeatMode getRepeatMode();
-    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
     property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
     property public final int iterations;
     property public final androidx.compose.animation.core.RepeatMode repeatMode;
   }
 
-  @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
     ctor public SnapSpec(int delay);
     ctor public SnapSpec();
     method public int getDelay();
@@ -443,7 +455,7 @@
   public final class SpringSimulationKt {
   }
 
-  @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public SpringSpec(float dampingRatio, float stiffness, T? visibilityThreshold);
     ctor public SpringSpec();
     method public float getDampingRatio();
@@ -489,6 +501,25 @@
   public final class ToolingGlueKt {
   }
 
+  public final class Transition<S> {
+    method public S! getCurrentState();
+    method public S! getTargetState();
+    method public androidx.compose.animation.core.Transition.States<S> getTransitionStates();
+    method public boolean isRunning();
+    property public final S! currentState;
+    property public final boolean isRunning;
+    property public final S! targetState;
+    property public final androidx.compose.animation.core.Transition.States<S> transitionStates;
+  }
+
+  public static final class Transition.States<S> {
+    ctor public Transition.States(S? initialState, S? targetState);
+    method public S! getInitialState();
+    method public S! getTargetState();
+    property public final S! initialState;
+    property public final S! targetState;
+  }
+
   public final class TransitionAnimation<T> implements androidx.compose.animation.core.TransitionState {
     ctor public TransitionAnimation(androidx.compose.animation.core.TransitionDefinition<T> def, androidx.compose.animation.core.AnimationClockObservable clock, T? initState, String? label);
     method public operator <T, V extends androidx.compose.animation.core.AnimationVector> T! get(androidx.compose.animation.core.PropKey<T,V> propKey);
@@ -520,14 +551,30 @@
 
   public final class TransitionDefinitionKt {
     method public static <T> androidx.compose.animation.core.TransitionAnimation<T> createAnimation(androidx.compose.animation.core.TransitionDefinition<T>, androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
-    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
-    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> snap(optional int delayMillis);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SpringSpec<T> spring(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
     method public static <T> androidx.compose.animation.core.TransitionDefinition<T> transitionDefinition(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionDefinition<T>,kotlin.Unit> init);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.TweenSpec<T> tween(optional int durationMillis, optional int delayMillis, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> easing);
   }
 
+  public final class TransitionKt {
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Bounds> animateBounds(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Bounds>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Bounds> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> animateDp(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Dp>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Dp> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Float> animateFloat(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Float> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Integer> animateInt(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Integer>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Integer> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntOffset> animateIntOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntOffset> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntSize> animateIntSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntSize> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Offset> animateOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Offset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Offset> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Position> animatePosition(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Position>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Position> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Rect> animateRect(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Rect> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Size> animateSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Size>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Size> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S, T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.runtime.State<T> animateValue(androidx.compose.animation.core.Transition<S>, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<T>> transitionSpec, kotlin.jvm.functions.Function1<? super S,? extends T> targetValueByState);
+    method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.Transition<T> updateTransition(T? targetState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onFinished);
+  }
+
   public final class TransitionSpec<S> {
     method public androidx.compose.animation.core.InterruptionHandling getInterruptionHandling();
     method public S? getNextState();
@@ -561,6 +608,17 @@
     property public abstract kotlin.jvm.functions.Function1<T,V> convertToVector;
   }
 
+  public final class VectorConvertersKt {
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+  }
+
   public interface VectorizedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> {
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public default V getEndVelocity(V start, V end, V startVelocity);
@@ -571,7 +629,7 @@
   public final class VectorizedAnimationSpecKt {
   }
 
-  public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     method public int getDelayMillis();
     method public int getDurationMillis();
     method public default long getDurationMillis(V start, V end, V startVelocity);
@@ -579,13 +637,20 @@
     property public abstract int durationMillis;
   }
 
-  public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public interface VectorizedFiniteAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  }
+
+  public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedFloatAnimationSpec(androidx.compose.animation.core.FloatAnimationSpec anim);
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public V getValue(long playTime, V start, V end, V startVelocity);
     method public V getVelocity(long playTime, V start, V end, V startVelocity);
   }
 
+  public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+    ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+  }
+
   public final class VectorizedKeyframesSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> {
     ctor public VectorizedKeyframesSpec(java.util.Map<java.lang.Integer,? extends kotlin.Pair<? extends V,? extends kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float>>> keyframes, int durationMillis, int delayMillis);
     method public int getDelayMillis();
@@ -596,7 +661,7 @@
     property public int durationMillis;
   }
 
-  public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedRepeatableSpec(int iterations, androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public V getValue(long playTime, V start, V end, V startVelocity);
@@ -614,7 +679,7 @@
     property public int durationMillis;
   }
 
-  public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedSpringSpec(float dampingRatio, float stiffness, V? visibilityThreshold);
     method public float getDampingRatio();
     method public float getStiffness();
@@ -635,5 +700,17 @@
     property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> easing;
   }
 
+  public final class VisibilityThresholdsKt {
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntOffset.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Offset.Companion);
+    method public static int getVisibilityThreshold(kotlin.jvm.internal.IntCompanionObject);
+    method public static float getVisibilityThreshold(androidx.compose.ui.unit.Dp.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.Position.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Size.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntSize.Companion);
+    method public static androidx.compose.ui.geometry.Rect getVisibilityThreshold(androidx.compose.ui.geometry.Rect.Companion);
+    method public static androidx.compose.ui.unit.Bounds getVisibilityThreshold(androidx.compose.ui.unit.Bounds.Companion);
+  }
+
 }
 
diff --git a/compose/animation/animation-core/api/public_plus_experimental_current.txt b/compose/animation/animation-core/api/public_plus_experimental_current.txt
index 2603144..41d1e7f 100644
--- a/compose/animation/animation-core/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-core/api/public_plus_experimental_current.txt
@@ -55,7 +55,7 @@
   public final class AnimationConstants {
     field public static final int DefaultDurationMillis = 300; // 0x12c
     field public static final androidx.compose.animation.core.AnimationConstants INSTANCE;
-    field public static final int Infinite = 2147483647; // 0x7fffffff
+    field @Deprecated public static final int Infinite = 2147483647; // 0x7fffffff
   }
 
   public enum AnimationEndReason {
@@ -115,6 +115,7 @@
     method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> AnimationState-SZyJPdc(float initialValue, optional float initialVelocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
     method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> copy-mN48zRs(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>, optional float value, optional float velocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
     method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.AnimationState<T,V> copy-upwry94(androidx.compose.animation.core.AnimationState<T,V>, optional T? value, optional V? velocityVector, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
+    method public static <T, V extends androidx.compose.animation.core.AnimationVector> V createZeroVectorFrom(androidx.compose.animation.core.TwoWayConverter<T,V>, T? value);
     method public static float getVelocity(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
     method public static float getVelocity(androidx.compose.animation.core.AnimationScope<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
     method public static boolean isFinished(androidx.compose.animation.core.AnimationState<?,?>);
@@ -235,7 +236,7 @@
     method public void dispatchTime$metalava_module(long frameTimeMillis);
   }
 
-  public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+  public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.FiniteAnimationSpec<T> {
     method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
   }
 
@@ -257,6 +258,10 @@
     property public float absVelocityThreshold;
   }
 
+  public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+  }
+
   public interface FloatAnimationSpec extends androidx.compose.animation.core.AnimationSpec<java.lang.Float> {
     method public long getDurationMillis(float start, float end, float startVelocity);
     method public default float getEndVelocity(float start, float end, float startVelocity);
@@ -309,6 +314,15 @@
     property public final int duration;
   }
 
+  public final class InfiniteRepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+    ctor public InfiniteRepeatableSpec(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+    method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
+    method public androidx.compose.animation.core.RepeatMode getRepeatMode();
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+    property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
+    property public final androidx.compose.animation.core.RepeatMode repeatMode;
+  }
+
   public final class IntPropKey implements androidx.compose.animation.core.PropKey<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> {
     ctor public IntPropKey(String label);
     ctor public IntPropKey();
@@ -394,8 +408,6 @@
 
   public final class PropKeyKt {
     method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.TwoWayConverter<T,V> TwoWayConverter(kotlin.jvm.functions.Function1<? super T,? extends V> convertToVector, kotlin.jvm.functions.Function1<? super V,? extends T> convertFromVector);
-    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getFloatToVectorConverter();
-    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getIntToVectorConverter();
     method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.FloatCompanionObject);
     method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.IntCompanionObject);
   }
@@ -405,18 +417,18 @@
     enum_constant public static final androidx.compose.animation.core.RepeatMode Reverse;
   }
 
-  @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public RepeatableSpec(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
     method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
     method public int getIterations();
     method public androidx.compose.animation.core.RepeatMode getRepeatMode();
-    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
     property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
     property public final int iterations;
     property public final androidx.compose.animation.core.RepeatMode repeatMode;
   }
 
-  @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
     ctor public SnapSpec(int delay);
     ctor public SnapSpec();
     method public int getDelay();
@@ -443,7 +455,7 @@
   public final class SpringSimulationKt {
   }
 
-  @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public SpringSpec(float dampingRatio, float stiffness, T? visibilityThreshold);
     ctor public SpringSpec();
     method public float getDampingRatio();
@@ -489,6 +501,25 @@
   public final class ToolingGlueKt {
   }
 
+  public final class Transition<S> {
+    method public S! getCurrentState();
+    method public S! getTargetState();
+    method public androidx.compose.animation.core.Transition.States<S> getTransitionStates();
+    method public boolean isRunning();
+    property public final S! currentState;
+    property public final boolean isRunning;
+    property public final S! targetState;
+    property public final androidx.compose.animation.core.Transition.States<S> transitionStates;
+  }
+
+  public static final class Transition.States<S> {
+    ctor public Transition.States(S? initialState, S? targetState);
+    method public S! getInitialState();
+    method public S! getTargetState();
+    property public final S! initialState;
+    property public final S! targetState;
+  }
+
   public final class TransitionAnimation<T> implements androidx.compose.animation.core.TransitionState {
     ctor public TransitionAnimation(androidx.compose.animation.core.TransitionDefinition<T> def, androidx.compose.animation.core.AnimationClockObservable clock, T? initState, String? label);
     method public operator <T, V extends androidx.compose.animation.core.AnimationVector> T! get(androidx.compose.animation.core.PropKey<T,V> propKey);
@@ -520,14 +551,30 @@
 
   public final class TransitionDefinitionKt {
     method public static <T> androidx.compose.animation.core.TransitionAnimation<T> createAnimation(androidx.compose.animation.core.TransitionDefinition<T>, androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
-    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
-    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> snap(optional int delayMillis);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SpringSpec<T> spring(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
     method public static <T> androidx.compose.animation.core.TransitionDefinition<T> transitionDefinition(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionDefinition<T>,kotlin.Unit> init);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.TweenSpec<T> tween(optional int durationMillis, optional int delayMillis, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> easing);
   }
 
+  public final class TransitionKt {
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Bounds> animateBounds(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Bounds>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Bounds> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> animateDp(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Dp>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Dp> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Float> animateFloat(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Float> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Integer> animateInt(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Integer>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Integer> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntOffset> animateIntOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntOffset> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntSize> animateIntSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntSize> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Offset> animateOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Offset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Offset> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Position> animatePosition(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Position>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Position> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Rect> animateRect(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Rect> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Size> animateSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Size>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Size> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S, T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.runtime.State<T> animateValue(androidx.compose.animation.core.Transition<S>, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<T>> transitionSpec, kotlin.jvm.functions.Function1<? super S,? extends T> targetValueByState);
+    method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.Transition<T> updateTransition(T? targetState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onFinished);
+  }
+
   public final class TransitionSpec<S> {
     method public androidx.compose.animation.core.InterruptionHandling getInterruptionHandling();
     method public S? getNextState();
@@ -561,6 +608,17 @@
     property public abstract kotlin.jvm.functions.Function1<T,V> convertToVector;
   }
 
+  public final class VectorConvertersKt {
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+  }
+
   public interface VectorizedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> {
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public default V getEndVelocity(V start, V end, V startVelocity);
@@ -571,7 +629,7 @@
   public final class VectorizedAnimationSpecKt {
   }
 
-  public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     method public int getDelayMillis();
     method public int getDurationMillis();
     method public default long getDurationMillis(V start, V end, V startVelocity);
@@ -579,13 +637,20 @@
     property public abstract int durationMillis;
   }
 
-  public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public interface VectorizedFiniteAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  }
+
+  public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedFloatAnimationSpec(androidx.compose.animation.core.FloatAnimationSpec anim);
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public V getValue(long playTime, V start, V end, V startVelocity);
     method public V getVelocity(long playTime, V start, V end, V startVelocity);
   }
 
+  public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+    ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+  }
+
   public final class VectorizedKeyframesSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> {
     ctor public VectorizedKeyframesSpec(java.util.Map<java.lang.Integer,? extends kotlin.Pair<? extends V,? extends kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float>>> keyframes, int durationMillis, int delayMillis);
     method public int getDelayMillis();
@@ -596,7 +661,7 @@
     property public int durationMillis;
   }
 
-  public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedRepeatableSpec(int iterations, androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public V getValue(long playTime, V start, V end, V startVelocity);
@@ -614,7 +679,7 @@
     property public int durationMillis;
   }
 
-  public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedSpringSpec(float dampingRatio, float stiffness, V? visibilityThreshold);
     method public float getDampingRatio();
     method public float getStiffness();
@@ -635,5 +700,17 @@
     property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> easing;
   }
 
+  public final class VisibilityThresholdsKt {
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntOffset.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Offset.Companion);
+    method public static int getVisibilityThreshold(kotlin.jvm.internal.IntCompanionObject);
+    method public static float getVisibilityThreshold(androidx.compose.ui.unit.Dp.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.Position.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Size.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntSize.Companion);
+    method public static androidx.compose.ui.geometry.Rect getVisibilityThreshold(androidx.compose.ui.geometry.Rect.Companion);
+    method public static androidx.compose.ui.unit.Bounds getVisibilityThreshold(androidx.compose.ui.unit.Bounds.Companion);
+  }
+
 }
 
diff --git a/compose/animation/animation-core/api/restricted_current.txt b/compose/animation/animation-core/api/restricted_current.txt
index 2603144..1855bc4 100644
--- a/compose/animation/animation-core/api/restricted_current.txt
+++ b/compose/animation/animation-core/api/restricted_current.txt
@@ -55,7 +55,7 @@
   public final class AnimationConstants {
     field public static final int DefaultDurationMillis = 300; // 0x12c
     field public static final androidx.compose.animation.core.AnimationConstants INSTANCE;
-    field public static final int Infinite = 2147483647; // 0x7fffffff
+    field @Deprecated public static final int Infinite = 2147483647; // 0x7fffffff
   }
 
   public enum AnimationEndReason {
@@ -115,6 +115,7 @@
     method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> AnimationState-SZyJPdc(float initialValue, optional float initialVelocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
     method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> copy-mN48zRs(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>, optional float value, optional float velocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
     method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.AnimationState<T,V> copy-upwry94(androidx.compose.animation.core.AnimationState<T,V>, optional T? value, optional V? velocityVector, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
+    method public static <T, V extends androidx.compose.animation.core.AnimationVector> V createZeroVectorFrom(androidx.compose.animation.core.TwoWayConverter<T,V>, T? value);
     method public static float getVelocity(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
     method public static float getVelocity(androidx.compose.animation.core.AnimationScope<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
     method public static boolean isFinished(androidx.compose.animation.core.AnimationState<?,?>);
@@ -235,7 +236,7 @@
     method public void dispatchTime$metalava_module(long frameTimeMillis);
   }
 
-  public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+  public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.FiniteAnimationSpec<T> {
     method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
   }
 
@@ -257,6 +258,10 @@
     property public float absVelocityThreshold;
   }
 
+  public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+  }
+
   public interface FloatAnimationSpec extends androidx.compose.animation.core.AnimationSpec<java.lang.Float> {
     method public long getDurationMillis(float start, float end, float startVelocity);
     method public default float getEndVelocity(float start, float end, float startVelocity);
@@ -309,6 +314,15 @@
     property public final int duration;
   }
 
+  public final class InfiniteRepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+    ctor public InfiniteRepeatableSpec(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+    method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
+    method public androidx.compose.animation.core.RepeatMode getRepeatMode();
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+    property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
+    property public final androidx.compose.animation.core.RepeatMode repeatMode;
+  }
+
   public final class IntPropKey implements androidx.compose.animation.core.PropKey<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> {
     ctor public IntPropKey(String label);
     ctor public IntPropKey();
@@ -394,8 +408,6 @@
 
   public final class PropKeyKt {
     method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.TwoWayConverter<T,V> TwoWayConverter(kotlin.jvm.functions.Function1<? super T,? extends V> convertToVector, kotlin.jvm.functions.Function1<? super V,? extends T> convertFromVector);
-    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getFloatToVectorConverter();
-    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getIntToVectorConverter();
     method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.FloatCompanionObject);
     method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.IntCompanionObject);
   }
@@ -405,18 +417,18 @@
     enum_constant public static final androidx.compose.animation.core.RepeatMode Reverse;
   }
 
-  @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public RepeatableSpec(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
     method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
     method public int getIterations();
     method public androidx.compose.animation.core.RepeatMode getRepeatMode();
-    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+    method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
     property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
     property public final int iterations;
     property public final androidx.compose.animation.core.RepeatMode repeatMode;
   }
 
-  @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
     ctor public SnapSpec(int delay);
     ctor public SnapSpec();
     method public int getDelay();
@@ -443,7 +455,7 @@
   public final class SpringSimulationKt {
   }
 
-  @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+  @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
     ctor public SpringSpec(float dampingRatio, float stiffness, T? visibilityThreshold);
     ctor public SpringSpec();
     method public float getDampingRatio();
@@ -489,6 +501,43 @@
   public final class ToolingGlueKt {
   }
 
+  public final class Transition<S> {
+    method @kotlin.PublishedApi internal boolean addAnimation(androidx.compose.animation.core.Transition<S>.TransitionAnimationState<?,?> animation);
+    method public S! getCurrentState();
+    method public S! getTargetState();
+    method public androidx.compose.animation.core.Transition.States<S> getTransitionStates();
+    method public boolean isRunning();
+    method @kotlin.PublishedApi internal void removeAnimation(androidx.compose.animation.core.Transition<S>.TransitionAnimationState<?,?> animation);
+    property public final S! currentState;
+    property public final boolean isRunning;
+    property public final S! targetState;
+    property public final androidx.compose.animation.core.Transition.States<S> transitionStates;
+  }
+
+  public static final class Transition.States<S> {
+    ctor public Transition.States(S? initialState, S? targetState);
+    method public S! getInitialState();
+    method public S! getTargetState();
+    property public final S! initialState;
+    property public final S! targetState;
+  }
+
+  @kotlin.PublishedApi internal final class Transition.TransitionAnimationState<T, V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.runtime.State<T> {
+    ctor @kotlin.PublishedApi internal Transition.TransitionAnimationState(T? initialValue, V initialVelocityVector, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter);
+    method public T! getTargetValue();
+    method public androidx.compose.animation.core.TwoWayConverter<T,V> getTypeConverter();
+    method public T! getValue();
+    method public V getVelocityVector();
+    method public boolean isFinished();
+    method @kotlin.PublishedApi internal void updateTargetValue(T? targetValue);
+    property public final boolean isFinished;
+    property public final T! targetValue;
+    property public final androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter;
+    property public T! value;
+    property public final V velocityVector;
+    field @kotlin.PublishedApi internal androidx.compose.animation.core.FiniteAnimationSpec<T> animationSpec;
+  }
+
   public final class TransitionAnimation<T> implements androidx.compose.animation.core.TransitionState {
     ctor public TransitionAnimation(androidx.compose.animation.core.TransitionDefinition<T> def, androidx.compose.animation.core.AnimationClockObservable clock, T? initState, String? label);
     method public operator <T, V extends androidx.compose.animation.core.AnimationVector> T! get(androidx.compose.animation.core.PropKey<T,V> propKey);
@@ -520,14 +569,30 @@
 
   public final class TransitionDefinitionKt {
     method public static <T> androidx.compose.animation.core.TransitionAnimation<T> createAnimation(androidx.compose.animation.core.TransitionDefinition<T>, androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
-    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
-    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> snap(optional int delayMillis);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
+    method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SpringSpec<T> spring(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
     method public static <T> androidx.compose.animation.core.TransitionDefinition<T> transitionDefinition(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionDefinition<T>,kotlin.Unit> init);
     method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.TweenSpec<T> tween(optional int durationMillis, optional int delayMillis, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> easing);
   }
 
+  public final class TransitionKt {
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Bounds> animateBounds(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Bounds>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Bounds> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> animateDp(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Dp>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Dp> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Float> animateFloat(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Float> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Integer> animateInt(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Integer>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Integer> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntOffset> animateIntOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntOffset> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntSize> animateIntSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntSize> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Offset> animateOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Offset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Offset> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Position> animatePosition(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Position>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Position> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Rect> animateRect(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Rect> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Size> animateSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Size>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Size> targetValueByState);
+    method @androidx.compose.runtime.Composable public static inline <S, T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.runtime.State<T> animateValue(androidx.compose.animation.core.Transition<S>, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<T>> transitionSpec, kotlin.jvm.functions.Function1<? super S,? extends T> targetValueByState);
+    method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.Transition<T> updateTransition(T? targetState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onFinished);
+  }
+
   public final class TransitionSpec<S> {
     method public androidx.compose.animation.core.InterruptionHandling getInterruptionHandling();
     method public S? getNextState();
@@ -561,6 +626,17 @@
     property public abstract kotlin.jvm.functions.Function1<T,V> convertToVector;
   }
 
+  public final class VectorConvertersKt {
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+  }
+
   public interface VectorizedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> {
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public default V getEndVelocity(V start, V end, V startVelocity);
@@ -571,7 +647,7 @@
   public final class VectorizedAnimationSpecKt {
   }
 
-  public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     method public int getDelayMillis();
     method public int getDurationMillis();
     method public default long getDurationMillis(V start, V end, V startVelocity);
@@ -579,13 +655,20 @@
     property public abstract int durationMillis;
   }
 
-  public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public interface VectorizedFiniteAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  }
+
+  public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedFloatAnimationSpec(androidx.compose.animation.core.FloatAnimationSpec anim);
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public V getValue(long playTime, V start, V end, V startVelocity);
     method public V getVelocity(long playTime, V start, V end, V startVelocity);
   }
 
+  public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+    ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+  }
+
   public final class VectorizedKeyframesSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> {
     ctor public VectorizedKeyframesSpec(java.util.Map<java.lang.Integer,? extends kotlin.Pair<? extends V,? extends kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float>>> keyframes, int durationMillis, int delayMillis);
     method public int getDelayMillis();
@@ -596,7 +679,7 @@
     property public int durationMillis;
   }
 
-  public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedRepeatableSpec(int iterations, androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
     method public long getDurationMillis(V start, V end, V startVelocity);
     method public V getValue(long playTime, V start, V end, V startVelocity);
@@ -614,7 +697,7 @@
     property public int durationMillis;
   }
 
-  public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+  public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
     ctor public VectorizedSpringSpec(float dampingRatio, float stiffness, V? visibilityThreshold);
     method public float getDampingRatio();
     method public float getStiffness();
@@ -635,5 +718,17 @@
     property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> easing;
   }
 
+  public final class VisibilityThresholdsKt {
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntOffset.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Offset.Companion);
+    method public static int getVisibilityThreshold(kotlin.jvm.internal.IntCompanionObject);
+    method public static float getVisibilityThreshold(androidx.compose.ui.unit.Dp.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.Position.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Size.Companion);
+    method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntSize.Companion);
+    method public static androidx.compose.ui.geometry.Rect getVisibilityThreshold(androidx.compose.ui.geometry.Rect.Companion);
+    method public static androidx.compose.ui.unit.Bounds getVisibilityThreshold(androidx.compose.ui.unit.Bounds.Companion);
+  }
+
 }
 
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 16c9b69..781604b3 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -17,7 +17,6 @@
 
 import androidx.build.AndroidXUiPlugin
 import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
 import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -56,7 +55,10 @@
 
         androidTestImplementation(ANDROIDX_TEST_RULES)
         androidTestImplementation(ANDROIDX_TEST_RUNNER)
+        androidTestImplementation(ANDROIDX_TEST_CORE)
         androidTestImplementation(JUNIT)
+        androidTestImplementation(project(":compose:animation:animation"))
+        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
     }
 }
 
@@ -96,7 +98,10 @@
             androidAndroidTest.dependencies {
                 implementation(ANDROIDX_TEST_RULES)
                 implementation(ANDROIDX_TEST_RUNNER)
+                implementation(ANDROIDX_TEST_CORE)
                 implementation(JUNIT)
+                implementation project(":compose:animation:animation")
+                implementation project(":compose:ui:ui-test-junit4")
             }
         }
     }
@@ -119,3 +124,14 @@
         ]
     }
 }
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+    tasks.withType(KotlinCompile).configureEach {
+        kotlinOptions {
+            useIR = true
+        }
+    }
+}
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
index cc0dda0..43d0a9e 100644
--- a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
@@ -17,14 +17,13 @@
 package androidx.compose.animation.core.samples
 
 import androidx.annotation.Sampled
-import androidx.compose.animation.core.AnimationConstants
 import androidx.compose.animation.core.AnimationState
 import androidx.compose.animation.core.RepeatMode
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.animate
 import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.isFinished
-import androidx.compose.animation.core.repeatable
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Box
@@ -82,8 +81,7 @@
             animate(
                 initialValue = 1f,
                 targetValue = 0f,
-                animationSpec = repeatable(
-                    iterations = AnimationConstants.Infinite,
+                animationSpec = infiniteRepeatable(
                     animation = tween(1000),
                     repeatMode = RepeatMode.Reverse
                 )
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt
new file mode 100644
index 0000000..61845d2
--- /dev/null
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.pressIndicatorGestureFilter
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.dp
+
+@Sampled
+@Composable
+fun GestureAnimationSample() {
+    // enum class ComponentState { Pressed, Released }
+    var useRed by remember { mutableStateOf(false) }
+    var toState by remember { mutableStateOf(ComponentState.Released) }
+    val modifier = Modifier.pressIndicatorGestureFilter(
+        onStart = { toState = ComponentState.Pressed },
+        onStop = { toState = ComponentState.Released },
+        onCancel = { toState = ComponentState.Released }
+    )
+
+    // Defines a transition of `ComponentState`, and updates the transition when the provided
+    // [targetState] changes. The transition will run all of the child animations towards the new
+    // [targetState] in response to the [targetState] change.
+    val transition: Transition<ComponentState> = updateTransition(targetState = toState)
+    // Defines a float animation as a child animation the transition. The current animation value
+    // can be read from the returned State<Float>.
+    val scale: Float by transition.animateFloat(
+        // Defines a transition spec that uses the same low-stiffness spring for *all*
+        // transitions of this float, no matter what the target is.
+        transitionSpec = { spring(stiffness = 50f) }
+    ) { state ->
+        // This code block declares a mapping from state to value.
+        if (state == ComponentState.Pressed) 3f else 1f
+    }
+
+    // Defines a color animation as a child animation of the transition.
+    val color: Color by transition.animateColor(
+        transitionSpec = { transitionStates ->
+            if (transitionStates.initialState == ComponentState.Pressed &&
+                transitionStates.targetState == ComponentState.Released
+            ) {
+                // Uses spring for the transition going from pressed to released
+                spring(stiffness = 50f)
+            } else {
+                // Uses tween for all the other transitions. (In this case there is
+                // only one other transition. i.e. released -> pressed.)
+                tween(durationMillis = 500)
+            }
+        }
+    ) { state ->
+        when (state) {
+            // Similar to the float animation, we need to declare the target values
+            // for each state. In this code block we can access theme colors.
+            ComponentState.Pressed -> MaterialTheme.colors.primary
+            // We can also have the target value depend on other mutableStates,
+            // such as `useRed` here. Whenever the target value changes, transition
+            // will automatically animate to the new value even if it has already
+            // arrived at its target state.
+            ComponentState.Released -> if (useRed) Color.Red else MaterialTheme.colors.secondary
+        }
+    }
+    Column {
+        Button(
+            modifier = Modifier.padding(10.dp).align(Alignment.CenterHorizontally),
+            onClick = { useRed = !useRed }
+        ) {
+            Text("Change Color")
+        }
+        Box(
+            modifier.fillMaxSize().wrapContentSize(Alignment.Center)
+                .size((100 * scale).dp).background(color)
+        )
+    }
+}
+
+private enum class ComponentState { Pressed, Released }
+private enum class ButtonStatus { Initial, Pressed, Released }
+
+@Sampled
+@Composable
+fun AnimateFloatSample() {
+    // enum class ButtonStatus {Initial, Pressed, Released}
+    @Composable
+    fun AnimateAlphaAndScale(
+        modifier: Modifier,
+        transition: Transition<ButtonStatus>
+    ) {
+        // Defines a float animation as a child animation of transition. This allows the
+        // transition to manage the states of this animation. The returned State<Float> from the
+        // [animateFloat] function is used here as a property delegate.
+        // This float animation will use the default [spring] for all transition destinations, as
+        // specified by the default `transitionSpec`.
+        val scale: Float by transition.animateFloat { state ->
+            if (state == ButtonStatus.Pressed) 1.2f else 1f
+        }
+
+        // Alternatively, we can specify different animation specs based on the initial state and
+        // target state of the a transition run using `transitionSpec`.
+        val alpha: Float by transition.animateFloat(
+            transitionSpec = {
+                if (it.initialState == ButtonStatus.Initial &&
+                    it.targetState == ButtonStatus.Pressed
+                ) {
+                    keyframes {
+                        durationMillis = 225
+                        0f at 0 // optional
+                        0.3f at 75
+                        0.2f at 225 // optional
+                    }
+                } else if (it.initialState == ButtonStatus.Pressed &&
+                    it.targetState == ButtonStatus.Released
+                ) {
+                    tween(durationMillis = 220)
+                } else {
+                    snap()
+                }
+            }
+        ) { state ->
+            // Same target value for Initial and Released states
+            if (state == ButtonStatus.Pressed) 0.2f else 0f
+        }
+
+        Box(modifier.graphicsLayer(alpha = alpha, scaleX = scale)) {
+            // content goes here
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/androidAndroidTest/AndroidManifest.xml b/compose/animation/animation-core/src/androidAndroidTest/AndroidManifest.xml
new file mode 100644
index 0000000..61f91c5
--- /dev/null
+++ b/compose/animation/animation-core/src/androidAndroidTest/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest package="androidx.compose.animation.core"/>
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
new file mode 100644
index 0000000..6d4d2ac
--- /dev/null
+++ b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.animation.VectorConverter
+import androidx.compose.animation.animateColor
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import junit.framework.TestCase.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class TransitionTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private enum class AnimStates {
+        From,
+        To
+    }
+
+    @Test
+    fun transitionTest() {
+        val target = mutableStateOf(AnimStates.From)
+        val floatAnim1 = TargetBasedAnimation(
+            spring(dampingRatio = Spring.DampingRatioHighBouncy),
+            0f,
+            1f,
+            Float.VectorConverter
+        )
+        val floatAnim2 = TargetBasedAnimation(
+            spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
+            1f,
+            0f,
+            Float.VectorConverter
+        )
+
+        val colorAnim1 = TargetBasedAnimation(
+            tween(1000),
+            Color.Red,
+            Color.Green,
+            Color.VectorConverter(Color.Red.colorSpace)
+        )
+        val colorAnim2 = TargetBasedAnimation(
+            tween(1000),
+            Color.Green,
+            Color.Red,
+            Color.VectorConverter(Color.Red.colorSpace)
+        )
+
+        // Animate from 0f to 0f for 1000ms
+        val keyframes1 = keyframes<Float> {
+            durationMillis = 1000
+            0f at 0
+            200f at 400
+            1000f at 1000
+        }
+
+        val keyframes2 = keyframes<Float> {
+            durationMillis = 800
+            0f at 0
+            -500f at 400
+            -1000f at 800
+        }
+
+        val keyframesAnim1 = TargetBasedAnimation(
+            keyframes1,
+            0f,
+            0f,
+            Float.VectorConverter
+        )
+        val keyframesAnim2 = TargetBasedAnimation(
+            keyframes2,
+            0f,
+            0f,
+            Float.VectorConverter
+        )
+        val animFloat = mutableStateOf(-1f)
+        val animColor = mutableStateOf(Color.Gray)
+        val animFloatWithKeyframes = mutableStateOf(-1f)
+        rule.setContent {
+            val transition = updateTransition(target.value)
+            animFloat.value = transition.animateFloat(
+                {
+                    if (it.initialState == AnimStates.From && it.targetState == AnimStates.To) {
+                        spring(dampingRatio = Spring.DampingRatioHighBouncy)
+                    } else {
+                        spring(
+                            dampingRatio = Spring.DampingRatioLowBouncy,
+                            stiffness = Spring.StiffnessLow
+                        )
+                    }
+                }
+            ) {
+                when (it) {
+                    AnimStates.From -> 0f
+                    AnimStates.To -> 1f
+                }
+            }.value
+
+            animColor.value = transition.animateColor(
+                { tween(durationMillis = 1000) }
+            ) {
+                when (it) {
+                    AnimStates.From -> Color.Red
+                    AnimStates.To -> Color.Green
+                }
+            }.value
+
+            animFloatWithKeyframes.value = transition.animateFloat(
+                transitionSpec = {
+                    if (it.initialState == AnimStates.From && it.targetState == AnimStates.To) {
+                        keyframes1
+                    } else {
+                        keyframes2
+                    }
+                }
+            ) {
+                // Same values for all states, but different transitions from state to state.
+                0f
+            }.value
+
+            if (transition.isRunning) {
+                if (transition.targetState == AnimStates.To) {
+                    assertEquals(
+                        floatAnim1.getValue(transition.playTimeNanos / 1_000_000L),
+                        animFloat.value, 0.00001f
+                    )
+                    assertEquals(
+                        colorAnim1.getValue(transition.playTimeNanos / 1_000_000L),
+                        animColor.value
+                    )
+                    assertEquals(
+                        keyframesAnim1.getValue(transition.playTimeNanos / 1_000_000L),
+                        animFloatWithKeyframes.value, 0.00001f
+                    )
+
+                    assertEquals(AnimStates.To, transition.transitionStates.targetState)
+                    assertEquals(AnimStates.From, transition.transitionStates.initialState)
+                } else {
+                    assertEquals(
+                        floatAnim2.getValue(transition.playTimeNanos / 1_000_000L),
+                        animFloat.value, 0.00001f
+                    )
+                    assertEquals(
+                        colorAnim2.getValue(transition.playTimeNanos / 1_000_000L),
+                        animColor.value
+                    )
+                    assertEquals(
+                        keyframesAnim2.getValue(transition.playTimeNanos / 1_000_000L),
+                        animFloatWithKeyframes.value, 0.00001f
+                    )
+                    assertEquals(AnimStates.From, transition.transitionStates.targetState)
+                    assertEquals(AnimStates.To, transition.transitionStates.initialState)
+                }
+            }
+        }
+
+        assertEquals(0f, animFloat.value)
+        assertEquals(Color.Red, animColor.value)
+        rule.runOnIdle {
+            target.value = AnimStates.To
+        }
+        rule.waitForIdle()
+
+        assertEquals(1f, animFloat.value)
+        assertEquals(Color.Green, animColor.value)
+
+        // Animate back to the `from` state
+        rule.runOnIdle {
+            target.value = AnimStates.From
+        }
+        rule.waitForIdle()
+
+        assertEquals(0f, animFloat.value)
+        assertEquals(Color.Red, animColor.value)
+    }
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt
index a457db21..e04c380 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt
@@ -185,8 +185,8 @@
         }
 
         lastFrameTime = timeMillis
-        value = anim.getValue(playtime)
         velocityVector = anim.getVelocityVector(playtime)
+        value = anim.getValue(playtime)
 
         checkFinished(playtime)
     }
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
index 8c71f54..7f7b55a 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
@@ -28,8 +28,13 @@
     const val DefaultDurationMillis: Int = 300
 
     /**
-     * Used as a iterations count for [VectorizedRepeatableSpec] to create an infinity repeating animation.
+     * Used as a iterations count for [VectorizedRepeatableSpec] to create an infinity repeating
+     * animation.
      */
+    @Deprecated(
+        "Using Infinite to specify repeatable animation iterations has been " +
+            "deprecated. Please use [InfiniteRepeatableSpec] or [infiniteRepeatable] instead."
+    )
     const val Infinite: Int = Int.MAX_VALUE
 }
 
@@ -65,6 +70,19 @@
 }
 
 /**
+ * [FiniteAnimationSpec] is the interface that all non-infinite [AnimationSpec]s implement,
+ * including: [TweenSpec], [SpringSpec], [KeyframesSpec], [RepeatableSpec], [SnapSpec], etc. By
+ * definition, [InfiniteRepeatableSpec] __does not__ implement this interface.
+ *
+ * @see [InfiniteRepeatableSpec]
+ */
+interface FiniteAnimationSpec<T> : AnimationSpec<T> {
+    override fun <V : AnimationVector> vectorize(
+        converter: TwoWayConverter<T, V>
+    ): VectorizedFiniteAnimationSpec<V>
+}
+
+/**
  * Creates a TweenSpec configured with the given duration, delay, and easing curve.
  *
  * @param durationMillis duration of the [VectorizedTweenSpec] animation.
@@ -100,7 +118,7 @@
  *  [TweenSpec], and [SnapSpec]. These duration based specs can repeated when put into a
  *  [RepeatableSpec].
  */
-interface DurationBasedAnimationSpec<T> : AnimationSpec<T> {
+interface DurationBasedAnimationSpec<T> : FiniteAnimationSpec<T> {
     override fun <V : AnimationVector> vectorize(converter: TwoWayConverter<T, V>):
         VectorizedDurationBasedAnimationSpec<V>
 }
@@ -114,12 +132,13 @@
  * @param stiffness stiffness of the spring. [Spring.StiffnessMedium] by default.
  * @param visibilityThreshold specifies the visibility threshold
  */
+// TODO: annotate damping/stiffness with FloatRange
 @Immutable
 class SpringSpec<T>(
     val dampingRatio: Float = Spring.DampingRatioNoBouncy,
     val stiffness: Float = Spring.StiffnessMedium,
     val visibilityThreshold: T? = null
-) : AnimationSpec<T> {
+) : FiniteAnimationSpec<T> {
 
     override fun <V : AnimationVector> vectorize(converter: TwoWayConverter<T, V>) =
         VectorizedSpringSpec(dampingRatio, stiffness, converter.convert(visibilityThreshold))
@@ -146,14 +165,18 @@
 }
 
 /**
- * [RepeatableSpec] takes another [DurationBasedAnimationSpec] and plays it [iterations] times.
+ * [RepeatableSpec] takes another [DurationBasedAnimationSpec] and plays it [iterations] times. For
+ * creating infinitely repeating animation spec, consider using [InfiniteRepeatableSpec].
  *
  * __Note__: When repeating in the [RepeatMode.Reverse] mode, it's highly recommended to have an
- * __odd__ number of iterations, or [AnimationConstants.Infinite] iterations. Otherwise, the
- * animation may jump to the end value when it finishes the last iteration.
+ * __odd__ number of iterations. Otherwise, the animation may jump to the end value when it finishes
+ * the last iteration.
  *
- * @param iterations the count of iterations. Should be at least 1. [AnimationConstants.Infinite]
- *                   can be used to have an infinity repeating animation.
+ * @see repeatable
+ * @see InfiniteRepeatableSpec
+ * @see infiniteRepeatable
+ *
+ * @param iterations the count of iterations. Should be at least 1.
  * @param animation the [AnimationSpec] to be repeated
  * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
  *                  [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
@@ -163,10 +186,10 @@
     val iterations: Int,
     val animation: DurationBasedAnimationSpec<T>,
     val repeatMode: RepeatMode = RepeatMode.Restart
-) : AnimationSpec<T> {
+) : FiniteAnimationSpec<T> {
     override fun <V : AnimationVector> vectorize(
         converter: TwoWayConverter<T, V>
-    ): VectorizedAnimationSpec<V> {
+    ): VectorizedFiniteAnimationSpec<V> {
         return VectorizedRepeatableSpec(iterations, animation.vectorize(converter), repeatMode)
     }
 
@@ -185,6 +208,42 @@
 }
 
 /**
+ * [InfiniteRepeatableSpec] repeats the provided [animation] infinite amount of times. It will
+ * never naturally finish. This means the animation will only be stopped via some form of manual
+ * cancellation. When used with transition or other animation composables, the infinite animations
+ * will stop when the composable is removed from the compose tree.
+ *
+ * For non-infinite repeating animations, consider [RepeatableSpec].
+ *
+ * @param animation the [AnimationSpec] to be repeated
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ *                  [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
+ * @see infiniteRepeatable
+ */
+// TODO: Consider supporting repeating spring specs
+class InfiniteRepeatableSpec<T>(
+    val animation: DurationBasedAnimationSpec<T>,
+    val repeatMode: RepeatMode = RepeatMode.Restart
+) : AnimationSpec<T> {
+    override fun <V : AnimationVector> vectorize(
+        converter: TwoWayConverter<T, V>
+    ): VectorizedAnimationSpec<V> {
+        return VectorizedInfiniteRepeatableSpec(animation.vectorize(converter), repeatMode)
+    }
+
+    override fun equals(other: Any?): Boolean =
+        if (other is RepeatableSpec<*>) {
+            other.animation == this.animation && other.repeatMode == this.repeatMode
+        } else {
+            false
+        }
+
+    override fun hashCode(): Int {
+        return animation.hashCode() * 31 + repeatMode.hashCode()
+    }
+}
+
+/**
  * Repeat mode for [RepeatableSpec] and [VectorizedRepeatableSpec].
  */
 enum class RepeatMode {
@@ -207,7 +266,7 @@
  *              starts. Defaults to 0.
  */
 @Immutable
-class SnapSpec<T>(val delay: Int = 0) : AnimationSpec<T> {
+class SnapSpec<T>(val delay: Int = 0) : DurationBasedAnimationSpec<T> {
     override fun <V : AnimationVector> vectorize(
         converter: TwoWayConverter<T, V>
     ): VectorizedDurationBasedAnimationSpec<V> = VectorizedSnapSpec(delay)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
index be201bb..945e9d4 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
@@ -54,7 +54,8 @@
     /**
      * Current velocity vector of the [AnimationState].
      */
-    var velocityVector: V = initialVelocityVector ?: typeConverter.createZeroVector(initialValue)
+    var velocityVector: V =
+        initialVelocityVector ?: typeConverter.createZeroVectorFrom(initialValue)
         internal set
 
     /**
@@ -243,5 +244,11 @@
     )
 }
 
-private fun <T, V : AnimationVector> TwoWayConverter<T, V>.createZeroVector(value: T) =
+/**
+ * Creates an AnimationVector with all the values set to 0 using the provided [TwoWayConverter]
+ * and the [value].
+ *
+ * @return a new AnimationVector instance of type [V].
+ */
+fun <T, V : AnimationVector> TwoWayConverter<T, V>.createZeroVectorFrom(value: T) =
     convertToVector(value).newInstance()
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
index 7b55b2e..05991c4 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
@@ -110,22 +110,6 @@
 
 /**
  * A [TwoWayConverter] that converts [Float] from and to [AnimationVector1D]
- * @see [Float.Companion.VectorConverter]
- */
-@Deprecated("", ReplaceWith("Float.VectorConverter"))
-val FloatToVectorConverter: TwoWayConverter<Float, AnimationVector1D> =
-    Float.VectorConverter
-
-/**
- * A [TwoWayConverter] that converts [Int] from and to [AnimationVector1D]
- * @see [Int.Companion.VectorConverter]
- */
-@Deprecated("", ReplaceWith("Int.VectorConverter"))
-val IntToVectorConverter: TwoWayConverter<Int, AnimationVector1D> =
-    Int.VectorConverter
-
-/**
- * A [TwoWayConverter] that converts [Float] from and to [AnimationVector1D]
  */
 val Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>
     get() = FloatToVector
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
new file mode 100644
index 0000000..c91c1dd
--- /dev/null
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -0,0 +1,624 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.runtime.dispatch.withFrameNanos
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Bounds
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Position
+import androidx.compose.ui.unit.Uptime
+import androidx.compose.ui.util.annotation.VisibleForTesting
+
+/**
+ * This sets up a [Transition], and updates it with the target provided by [targetState]. When
+ * [targetState] changes, [Transition] will run all of its child animations towards their
+ * target values specified for the new [targetState]. Child animations can be dynamically added
+ * using [Transition.animateFloat], [animateColor][ androidx.compose.animation.animateColor],
+ * [Transition.animateValue], etc.
+ *
+ * When all the animations in the transition have finished running, the provided [onFinished] will
+ * be invoked.
+ *
+ * @sample androidx.compose.animation.core.samples.GestureAnimationSample
+ *
+ * @return a [Transition] object, to which animations can be added.
+ * @see Transition
+ * @see animateFloat
+ * @see animateValue
+ * @see androidx.compose.animation.animateColor
+ */
+@Composable
+fun <T> updateTransition(
+    targetState: T,
+    onFinished: (T) -> Unit = {}
+): Transition<T> {
+    val listener = rememberUpdatedState(onFinished)
+    val transition = remember { Transition(targetState, listener) }
+    // This is needed because child animations rely on this target state and the state pair to
+    // update their animation specs
+    transition.updateTarget(targetState)
+    SideEffect {
+        transition.animateTo(targetState)
+    }
+    if (transition.isRunning || transition.startRequested) {
+        LaunchedEffect(transition) {
+            while (true) {
+                withFrameNanos {
+                    transition.onFrame(it)
+                }
+            }
+        }
+    }
+    return transition
+}
+
+/**
+ * [Transition] manages all the child animations on a state level. Child animations
+ * can be created in a declarative way using [animateFloat], [animateValue],
+ * [animateColor][androidx.compose.animation.animateColor] etc. When the [targetState] changes,
+ * [Transition] will automatically start or adjust course for all its child animations to animate
+ * to the new target values defined for each animation.
+ *
+ * After arriving at [targetState], [Transition] will be triggered to run if any child animation
+ * changes its target value (due to their dynamic target calculation logic, such as theme-dependent
+ * values).
+ *
+ * @sample androidx.compose.animation.core.samples.GestureAnimationSample
+ *
+ * @return a [Transition] object, to which animations can be added.
+ * @see updateTransition
+ * @see animateFloat
+ * @see animateValue
+ * @see androidx.compose.animation.animateColor
+ */
+// TODO: Support creating Transition outside of composition and support imperative use of Transition
+class Transition<S> internal constructor(
+    initialState: S,
+    private val onFinished: State<(S) -> Unit>
+) {
+    /**
+     * Current state of the transition. This will always be the initialState of the transition
+     * until the transition is finished. Once the transition is finished, [currentState] will be
+     * set to [targetState].
+     */
+    var currentState: S by mutableStateOf(initialState)
+        internal set
+
+    /**
+     * Target state of the transition. This will be read by all child animations to determine their
+     * most up-to-date target values.
+     */
+    var targetState: S by mutableStateOf(initialState)
+        internal set
+
+    /**
+     * [transitionStates] contains the initial state and the target state of the currently on-going
+     * transition.
+     */
+    var transitionStates: States<S> by mutableStateOf(States(initialState, initialState))
+        private set
+
+    /**
+     * Indicates whether there is any animation running in the transition.
+     */
+    val isRunning: Boolean
+        get() = startTime != Uptime.Unspecified
+
+    /**
+     * Play time in nano-seconds. [playTimeNanos] is always non-negative. It starts from 0L at the
+     * beginning of the transition and increment until all child animations have finished.
+     */
+    @VisibleForTesting
+    internal var playTimeNanos by mutableStateOf(0L)
+    internal var startRequested: Boolean by mutableStateOf(false)
+    private var startTime = Uptime.Unspecified
+    private val animations = mutableVectorOf<TransitionAnimationState<*, *>>()
+
+    // Target state that is currently being animated to
+    private var currentTargetState: S = initialState
+
+    internal fun onFrame(frameTimeNanos: Long) {
+        if (startTime == Uptime.Unspecified) {
+            startTime = Uptime(frameTimeNanos)
+        }
+        startRequested = false
+
+        // Update play time
+        playTimeNanos = frameTimeNanos - startTime.nanoseconds
+        var allFinished = true
+        // Pulse new playtime
+        animations.forEach {
+            if (!it.isFinished) {
+                it.onPlayTimeChanged(playTimeNanos)
+            }
+            // Check isFinished flag again after the animation pulse
+            if (!it.isFinished) {
+                allFinished = false
+            }
+        }
+        if (allFinished) {
+            startTime = Uptime.Unspecified
+            currentState = targetState
+            playTimeNanos = 0
+            onFinished.value(targetState)
+        }
+    }
+
+    @PublishedApi
+    internal fun addAnimation(animation: TransitionAnimationState<*, *>) =
+        animations.add(animation)
+
+    @PublishedApi
+    internal fun removeAnimation(animation: TransitionAnimationState<*, *>) {
+        animations.remove(animation)
+    }
+
+    // This target state should only be used to modify "mutableState"s, as it could potentially
+    // roll back. The
+    internal fun updateTarget(targetState: S) {
+        if (transitionStates.targetState != targetState) {
+            if (currentState == targetState) {
+                // Going backwards
+                transitionStates = States(this.targetState, targetState)
+            } else {
+                transitionStates = States(currentState, targetState)
+            }
+        }
+        this.targetState = targetState
+    }
+
+    internal fun animateTo(targetState: S) {
+        if (targetState != currentTargetState) {
+            if (isRunning) {
+                startTime = Uptime(startTime.nanoseconds + playTimeNanos)
+                playTimeNanos = 0
+            } else {
+                startRequested = true
+            }
+            currentTargetState = targetState
+            // If target state is changed, reset all the animations to be re-created in the
+            // next frame w/ their new target value. Child animations target values are updated in
+            // the side effect that may not have happened when this function in invoked.
+            animations.forEach { it.resetAnimation() }
+        }
+    }
+
+    // Called from children to start an animation
+    private fun requestStart() {
+        startRequested = true
+    }
+
+    // TODO: Consider making this public
+    @PublishedApi
+    internal inner class TransitionAnimationState<T, V : AnimationVector> @PublishedApi internal
+    constructor(
+        initialValue: T,
+        initialVelocityVector: V,
+        val typeConverter: TwoWayConverter<T, V>
+    ) : State<T> {
+
+        override var value by mutableStateOf(initialValue)
+            internal set
+
+        var targetValue: T = initialValue
+            internal set
+        var velocityVector: V = initialVelocityVector
+            internal set
+        var isFinished: Boolean by mutableStateOf(true)
+            private set
+        private var animation: Animation<T, V>? = null
+
+        @PublishedApi
+        internal var animationSpec: FiniteAnimationSpec<T> = spring()
+        private var offsetTimeNanos = 0L
+
+        internal fun onPlayTimeChanged(playTimeNanos: Long) {
+            val anim = animation ?: TargetBasedAnimation<T, V>(
+                animationSpec,
+                value,
+                targetValue,
+                typeConverter,
+                velocityVector
+            ).also { animation = it }
+            val playTimeMillis = (playTimeNanos - offsetTimeNanos) / 1_000_000L
+            value = anim.getValue(playTimeMillis)
+            velocityVector = anim.getVelocityVector(playTimeMillis)
+            if (anim.isFinished(playTimeMillis)) {
+                isFinished = true
+                offsetTimeNanos = 0
+            }
+        }
+
+        internal fun resetAnimation() {
+            animation = null
+            offsetTimeNanos = 0
+            isFinished = false
+        }
+
+        @PublishedApi
+        // This gets called from a side effect.
+        internal fun updateTargetValue(targetValue: T) {
+            if (this.targetValue != targetValue) {
+                this.targetValue = targetValue
+                isFinished = false
+                animation = null
+                offsetTimeNanos = playTimeNanos
+                requestStart()
+            }
+        }
+    }
+
+    /**
+     * [States] holds [initialState] and [targetState], which are the beginning and end of a
+     * transition. These states will be used to obtain the animation spec that will be used for this
+     * transition from the child animations.
+     */
+    class States<S>(val initialState: S, val targetState: S)
+}
+
+/**
+ * Creates an animation of type [T] as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition]. [typeConverter] will be used to convert
+ * between type [T] and [AnimationVector] so that the animation system knows how to animate it.
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ * @see updateTransition
+ * @see animateFloat
+ * @see androidx.compose.animation.animateColor
+ */
+@Composable
+inline fun <S, T, V : AnimationVector> Transition<S>.animateValue(
+    typeConverter: TwoWayConverter<T, V>,
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<T> =
+        { spring() },
+    targetValueByState: @Composable (state: S) -> T
+): State<T> {
+    val targetValue = targetValueByState(targetState)
+    val transitionAnimation = remember {
+        TransitionAnimationState(
+            targetValue,
+            typeConverter.createZeroVectorFrom(targetValue),
+            typeConverter
+        )
+    }
+    val spec = transitionSpec(transitionStates)
+
+    SideEffect {
+        transitionAnimation.animationSpec = spec
+        transitionAnimation.updateTargetValue(targetValue)
+    }
+
+    DisposableEffect(transitionAnimation) {
+        addAnimation(transitionAnimation)
+        onDispose {
+            removeAnimation(transitionAnimation)
+        }
+    }
+    return transitionAnimation
+}
+
+// TODO: Remove noinline when b/174814083 is fixed.
+/**
+ * Creates a Float animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @sample androidx.compose.animation.core.samples.AnimateFloatSample
+ *
+ * @return A [State] object, the value of which is updated by animation
+ * @see updateTransition
+ * @see animateValue
+ * @see androidx.compose.animation.animateColor
+ */
+@Composable
+inline fun <S> Transition<S>.animateFloat(
+    noinline transitionSpec:
+        @Composable (Transition.States<S>) -> FiniteAnimationSpec<Float> = { spring() },
+    targetValueByState: @Composable (state: S) -> Float
+): State<Float> =
+    animateValue(Float.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Dp] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateDp(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Dp> = {
+        spring(visibilityThreshold = Dp.VisibilityThreshold)
+    },
+    targetValueByState: @Composable (state: S) -> Dp
+): State<Dp> =
+    animateValue(Dp.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates an [Offset] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateOffset(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Offset> = {
+        spring(visibilityThreshold = Offset.VisibilityThreshold)
+    },
+    targetValueByState: @Composable (state: S) -> Offset
+): State<Offset> =
+    animateValue(Offset.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Position] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animatePosition(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Position> = {
+        spring(visibilityThreshold = Position.VisibilityThreshold)
+    },
+    targetValueByState: @Composable (state: S) -> Position
+): State<Position> =
+    animateValue(Position.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Size] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateSize(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Size> = {
+        spring(visibilityThreshold = Size.VisibilityThreshold)
+    },
+    targetValueByState: @Composable (state: S) -> Size
+): State<Size> =
+    animateValue(Size.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [IntOffset] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateIntOffset(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<IntOffset> =
+        { spring(visibilityThreshold = IntOffset(1, 1)) },
+    targetValueByState: @Composable (state: S) -> IntOffset
+): State<IntOffset> =
+    animateValue(IntOffset.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Int] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateInt(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Int> = {
+        spring(visibilityThreshold = 1)
+    },
+    targetValueByState: @Composable (state: S) -> Int
+): State<Int> =
+    animateValue(Int.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [IntSize] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateIntSize(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<IntSize> = {
+        spring(visibilityThreshold = IntSize(1, 1))
+    },
+    targetValueByState: @Composable (state: S) -> IntSize
+): State<IntSize> =
+    animateValue(IntSize.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Bounds] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateBounds(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Bounds> = {
+        spring(visibilityThreshold = Bounds.VisibilityThreshold)
+    },
+    targetValueByState: @Composable (state: S) -> Bounds
+): State<Bounds> =
+    animateValue(Bounds.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Rect] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateRect(
+    noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Rect> =
+        { spring(visibilityThreshold = Rect.VisibilityThreshold) },
+    targetValueByState: @Composable (state: S) -> Rect
+): State<Rect> =
+    animateValue(Rect.VectorConverter, transitionSpec, targetValueByState)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt
index 91b09ba..bc77654 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt
@@ -17,7 +17,6 @@
 package androidx.compose.animation.core
 
 import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
-import androidx.compose.animation.core.AnimationConstants.Infinite
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.util.fastFirstOrNull
 
@@ -149,11 +148,11 @@
  * [TweenSpec], [KeyframesSpec]) the amount of iterations specified by [iterations].
  *
  * The iteration count describes the amount of times the animation will run.
- * 1 means no repeat. Use [Infinite] to create an infinity repeating animation.
+ * 1 means no repeat. Recommend [infiniteRepeatable] for creating an infinity repeating animation.
  *
  * __Note__: When repeating in the [RepeatMode.Reverse] mode, it's highly recommended to have an
- * __odd__ number of iterations, or [AnimationConstants.Infinite] iterations. Otherwise, the
- * animation may jump to the end value when it finishes the last iteration.
+ * __odd__ number of iterations. Otherwise, the animation may jump to the end value when it finishes
+ * the last iteration.
  *
  * @param iterations the total count of iterations, should be greater than 1 to repeat.
  * @param animation animation that will be repeated
@@ -165,16 +164,33 @@
     iterations: Int,
     animation: DurationBasedAnimationSpec<T>,
     repeatMode: RepeatMode = RepeatMode.Restart
-): AnimationSpec<T> =
+): RepeatableSpec<T> =
     RepeatableSpec(iterations, animation, repeatMode)
 
 /**
+ * Creates a [InfiniteRepeatableSpec] that plays a [DurationBasedAnimationSpec] (e.g.
+ * [TweenSpec], [KeyframesSpec]) infinite amount of iterations.
+ *
+ * For non-infinitely repeating animations, consider [repeatable].
+ *
+ * @param animation animation that will be repeated
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ *                  [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
+ */
+@Stable
+fun <T> infiniteRepeatable(
+    animation: DurationBasedAnimationSpec<T>,
+    repeatMode: RepeatMode = RepeatMode.Restart
+): AnimationSpec<T> =
+    InfiniteRepeatableSpec(animation, repeatMode)
+
+/**
  * Creates a Snap animation for immediately switching the animating value to the end value.
  *
  * @param delayMillis the number of milliseconds to wait before the animation runs. 0 by default.
  */
 @Stable
-fun <T> snap(delayMillis: Int = 0): AnimationSpec<T> = SnapSpec(delayMillis)
+fun <T> snap(delayMillis: Int = 0) = SnapSpec<T>(delayMillis)
 
 /**
  * [TransitionDefinition] contains all the animation related configurations that will be used in
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt
new file mode 100644
index 0000000..585df63
--- /dev/null
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Bounds
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Position
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+/**
+ * A type converter that converts a [Rect] to a [AnimationVector4D], and vice versa.
+ */
+val Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>
+    get() = RectToVector
+
+/**
+ * A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
+ */
+val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
+    get() = DpToVector
+
+/**
+ * A type converter that converts a [Position] to a [AnimationVector2D], and vice versa.
+ */
+val Position.Companion.VectorConverter: TwoWayConverter<Position, AnimationVector2D>
+    get() = PositionToVector
+
+/**
+ * A type converter that converts a [Size] to a [AnimationVector2D], and vice versa.
+ */
+val Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>
+    get() = SizeToVector
+
+/**
+ * A type converter that converts a [Bounds] to a [AnimationVector4D], and vice versa.
+ */
+val Bounds.Companion.VectorConverter: TwoWayConverter<Bounds, AnimationVector4D>
+    get() = BoundsToVector
+
+/**
+ * A type converter that converts a [Offset] to a [AnimationVector2D], and vice versa.
+ */
+val Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>
+    get() = OffsetToVector
+
+/**
+ * A type converter that converts a [IntOffset] to a [AnimationVector2D], and vice versa.
+ */
+val IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>
+    get() = IntOffsetToVector
+
+/**
+ * A type converter that converts a [IntSize] to a [AnimationVector2D], and vice versa.
+ */
+val IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>
+    get() = IntSizeToVector
+
+/**
+ * A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
+ */
+private val DpToVector: TwoWayConverter<Dp, AnimationVector1D> = TwoWayConverter(
+    convertToVector = { AnimationVector1D(it.value) },
+    convertFromVector = { Dp(it.value) }
+)
+
+/**
+ * A type converter that converts a [Position] to a [AnimationVector2D], and vice versa.
+ */
+private val PositionToVector: TwoWayConverter<Position, AnimationVector2D> =
+    TwoWayConverter(
+        convertToVector = { AnimationVector2D(it.x.value, it.y.value) },
+        convertFromVector = { Position(it.v1.dp, it.v2.dp) }
+    )
+
+/**
+ * A type converter that converts a [Size] to a [AnimationVector2D], and vice versa.
+ */
+private val SizeToVector: TwoWayConverter<Size, AnimationVector2D> =
+    TwoWayConverter(
+        convertToVector = { AnimationVector2D(it.width, it.height) },
+        convertFromVector = { Size(it.v1, it.v2) }
+    )
+
+/**
+ * A type converter that converts a [Bounds] to a [AnimationVector4D], and vice versa.
+ */
+private val BoundsToVector: TwoWayConverter<Bounds, AnimationVector4D> =
+    TwoWayConverter(
+        convertToVector = {
+            AnimationVector4D(it.left.value, it.top.value, it.right.value, it.bottom.value)
+        },
+        convertFromVector = { Bounds(it.v1.dp, it.v2.dp, it.v3.dp, it.v4.dp) }
+    )
+
+/**
+ * A type converter that converts a [Offset] to a [AnimationVector2D], and vice versa.
+ */
+private val OffsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
+    TwoWayConverter(
+        convertToVector = { AnimationVector2D(it.x, it.y) },
+        convertFromVector = { Offset(it.v1, it.v2) }
+    )
+
+/**
+ * A type converter that converts a [IntOffset] to a [AnimationVector2D], and vice versa.
+ */
+private val IntOffsetToVector: TwoWayConverter<IntOffset, AnimationVector2D> =
+    TwoWayConverter(
+        convertToVector = { AnimationVector2D(it.x.toFloat(), it.y.toFloat()) },
+        convertFromVector = { IntOffset(it.v1.roundToInt(), it.v2.roundToInt()) }
+    )
+
+/**
+ * A type converter that converts a [IntSize] to a [AnimationVector2D], and vice versa.
+ */
+private val IntSizeToVector: TwoWayConverter<IntSize, AnimationVector2D> =
+    TwoWayConverter(
+        { AnimationVector2D(it.width.toFloat(), it.height.toFloat()) },
+        { IntSize(it.v1.roundToInt(), it.v2.roundToInt()) }
+    )
+
+/**
+ * A type converter that converts a [Rect] to a [AnimationVector4D], and vice versa.
+ */
+private val RectToVector: TwoWayConverter<Rect, AnimationVector4D> =
+    TwoWayConverter(
+        convertToVector = {
+            AnimationVector4D(it.left, it.top, it.right, it.bottom)
+        },
+        convertFromVector = {
+            Rect(it.v1, it.v2, it.v3, it.v4)
+        }
+    )
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index fd8d44d..bd1e235 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -17,7 +17,6 @@
 package androidx.compose.animation.core
 
 import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
-import androidx.compose.animation.core.AnimationConstants.Infinite
 import kotlin.math.min
 
 /**
@@ -107,13 +106,23 @@
 }
 
 /**
+ * All the finite [VectorizedAnimationSpec]s implement this interface, including:
+ * [VectorizedKeyframesSpec], [VectorizedTweenSpec], [VectorizedRepeatableSpec],
+ * [VectorizedSnapSpec], [VectorizedSpringSpec], etc. The [VectorizedAnimationSpec] that does
+ * __not__ implement this is: [InfiniteRepeatableSpec].
+ */
+interface VectorizedFiniteAnimationSpec<V : AnimationVector> : VectorizedAnimationSpec<V>
+
+/**
  * Base class for [VectorizedAnimationSpec]s that are based on a fixed [durationMillis].
  */
-interface VectorizedDurationBasedAnimationSpec<V : AnimationVector> : VectorizedAnimationSpec<V> {
+interface VectorizedDurationBasedAnimationSpec<V : AnimationVector> :
+    VectorizedFiniteAnimationSpec<V> {
     /**
      * duration is the amount of time while animation is not yet finished.
      */
     val durationMillis: Int
+
     /**
      * delay defines the amount of time that animation can be delayed.
      */
@@ -265,19 +274,41 @@
         get() = 0
 }
 
+private const val InfiniteIterations: Int = Int.MAX_VALUE
+
 /**
  * This animation takes another [VectorizedDurationBasedAnimationSpec] and plays it
- * [iterations] times.
+ * __infinite__ times.
  *
- * @param iterations the count of iterations. Should be at least 1. [Infinite] can
- *                   be used to have an infinity repeating animation.
  * @param animation the [VectorizedAnimationSpec] describing each repetition iteration.
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ *                  [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
+ */
+class VectorizedInfiniteRepeatableSpec<V : AnimationVector>(
+    private val animation: VectorizedDurationBasedAnimationSpec<V>,
+    private val repeatMode: RepeatMode = RepeatMode.Restart
+) : VectorizedAnimationSpec<V> by
+    VectorizedRepeatableSpec<V>(InfiniteIterations, animation, repeatMode)
+
+/**
+ * This animation takes another [VectorizedDurationBasedAnimationSpec] and plays it
+ * [iterations] times. For infinitely repeating animation spec, [VectorizedInfiniteRepeatableSpec]
+ * is recommended.
+ *
+ * __Note__: When repeating in the [RepeatMode.Reverse] mode, it's highly recommended to have an
+ * __odd__ number of iterations. Otherwise, the animation may jump to the end value when it finishes
+ * the last iteration.
+ *
+ * @param iterations the count of iterations. Should be at least 1.
+ * @param animation the [VectorizedAnimationSpec] describing each repetition iteration.
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ *                  [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
  */
 class VectorizedRepeatableSpec<V : AnimationVector>(
     private val iterations: Int,
     private val animation: VectorizedDurationBasedAnimationSpec<V>,
     private val repeatMode: RepeatMode = RepeatMode.Restart
-) : VectorizedAnimationSpec<V> {
+) : VectorizedFiniteAnimationSpec<V> {
 
     init {
         if (iterations < 1) {
@@ -345,15 +376,18 @@
      * Stiffness constant for extremely stiff spring
      */
     const val StiffnessHigh = 10_000f
+
     /**
      * Stiffness constant for medium stiff spring. This is the default stiffness for spring
      * force.
      */
     const val StiffnessMedium = 1500f
+
     /**
      * Stiffness constant for a spring with low stiffness.
      */
     const val StiffnessLow = 200f
+
     /**
      * Stiffness constant for a spring with very low stiffness.
      */
@@ -364,23 +398,27 @@
      * (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
      */
     const val DampingRatioHighBouncy = 0.2f
+
     /**
      * Damping ratio for a medium bouncy spring. This is also the default damping ratio for
      * spring force. Note for under-damped springs (i.e. damping ratio < 1), the lower the
      * damping ratio, the more bouncy the spring.
      */
     const val DampingRatioMediumBouncy = 0.5f
+
     /**
      * Damping ratio for a spring with low bounciness. Note for under-damped springs
      * (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
      */
     const val DampingRatioLowBouncy = 0.75f
+
     /**
      * Damping ratio for a spring with no bounciness. This damping ratio will create a
      * critically damped spring that returns to equilibrium within the shortest amount of time
      * without oscillating.
      */
     const val DampingRatioNoBouncy = 1f
+
     /**
      * Default cutoff for rounding off physics based animations
      */
@@ -401,7 +439,7 @@
     val dampingRatio: Float,
     val stiffness: Float,
     anims: Animations
-) : VectorizedAnimationSpec<V> by VectorizedFloatAnimationSpec<V>(anims) {
+) : VectorizedFiniteAnimationSpec<V> by VectorizedFloatAnimationSpec<V>(anims) {
 
     /**
      * Creates a [VectorizedSpringSpec] that uses the same spring constants (i.e. [dampingRatio] and
@@ -482,7 +520,7 @@
  */
 class VectorizedFloatAnimationSpec<V : AnimationVector> internal constructor(
     private val anims: Animations
-) : VectorizedAnimationSpec<V> {
+) : VectorizedFiniteAnimationSpec<V> {
     private lateinit var valueVector: V
     private lateinit var velocityVector: V
     private lateinit var endVelocityVector: V
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt
new file mode 100644
index 0000000..cc50e01
--- /dev/null
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Bounds
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Position
+import androidx.compose.ui.unit.dp
+
+private const val DpVisibilityThreshold = 0.1f
+private const val PxVisibilityThreshold = 0.5f
+
+private val boundsVisibilityThreshold = Bounds(
+    Dp.VisibilityThreshold,
+    Dp.VisibilityThreshold,
+    Dp.VisibilityThreshold,
+    Dp.VisibilityThreshold
+)
+
+private val rectVisibilityThreshold = Rect(
+    PxVisibilityThreshold,
+    PxVisibilityThreshold,
+    PxVisibilityThreshold,
+    PxVisibilityThreshold
+)
+
+/**
+ * Visibility threshold for [IntOffset]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val IntOffset.Companion.VisibilityThreshold: IntOffset
+    get() = IntOffset(1, 1)
+
+/**
+ * Visibility threshold for [Offset]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Offset.Companion.VisibilityThreshold: Offset
+    get() = Offset(PxVisibilityThreshold, PxVisibilityThreshold)
+
+/**
+ * Visibility threshold for [Int]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Int.Companion.VisibilityThreshold: Int
+    get() = 1
+
+/**
+ * Visibility threshold for [Dp]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Dp.Companion.VisibilityThreshold: Dp
+    get() = DpVisibilityThreshold.dp
+
+/**
+ * Visibility threshold for [Position]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Position.Companion.VisibilityThreshold: Position
+    get() = Position(Dp.VisibilityThreshold, Dp.VisibilityThreshold)
+
+/**
+ * Visibility threshold for [Size]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Size.Companion.VisibilityThreshold: Size
+    get() = Size(PxVisibilityThreshold, PxVisibilityThreshold)
+
+/**
+ * Visibility threshold for [IntSize]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val IntSize.Companion.VisibilityThreshold: IntSize
+    get() = IntSize(1, 1)
+
+/**
+ * Visibility threshold for [Rect]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Rect.Companion.VisibilityThreshold: Rect
+    get() = rectVisibilityThreshold
+
+/**
+ * Visibility threshold for [Bounds]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Bounds.Companion.VisibilityThreshold: Bounds
+    get() = boundsVisibilityThreshold
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
index 671941d..6524f1f 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
@@ -104,6 +104,25 @@
         assertEquals(100f, repeatAnim.getValue(901))
     }
 
+    @Test
+    fun testInfiniteRepeat() {
+        val repeat = infiniteRepeatable(
+            animation = TweenSpec<Float>(
+                durationMillis = 100, easing = LinearEasing
+            ),
+            repeatMode = RepeatMode.Reverse
+        )
+
+        assertEquals(
+            Int.MAX_VALUE.toLong() * 100,
+            repeat.vectorize(Float.VectorConverter).getDurationMillis(
+                AnimationVector(0f),
+                AnimationVector(100f),
+                AnimationVector(0f)
+            )
+        )
+    }
+
     private companion object {
         private val DelayDuration = 13
         private val Duration = 50
diff --git a/compose/animation/animation/api/current.txt b/compose/animation/animation/api/current.txt
index d9e7a92..3922f7a 100644
--- a/compose/animation/animation/api/current.txt
+++ b/compose/animation/animation/api/current.txt
@@ -94,10 +94,6 @@
   @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalAnimationApi {
   }
 
-  public final class LegacyTransitionKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static <T> void Transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionState,kotlin.Unit> children);
-  }
-
   public final class OffsetPropKey implements androidx.compose.animation.core.PropKey<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> {
     ctor public OffsetPropKey(String label);
     ctor public OffsetPropKey();
@@ -109,14 +105,14 @@
 
   public final class PropertyKeysKt {
     method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
   }
 
   public final class PxPropKey implements androidx.compose.animation.core.PropKey<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> {
@@ -154,6 +150,7 @@
   }
 
   public final class TransitionKt {
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColor(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.graphics.Color>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.graphics.Color> targetValueByState);
     method @Deprecated @VisibleForTesting public static void setTransitionsEnabled(boolean p);
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.TransitionState transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional String? label, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished);
   }
diff --git a/compose/animation/animation/api/public_plus_experimental_current.txt b/compose/animation/animation/api/public_plus_experimental_current.txt
index d9e7a92..3922f7a 100644
--- a/compose/animation/animation/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation/api/public_plus_experimental_current.txt
@@ -94,10 +94,6 @@
   @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalAnimationApi {
   }
 
-  public final class LegacyTransitionKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static <T> void Transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionState,kotlin.Unit> children);
-  }
-
   public final class OffsetPropKey implements androidx.compose.animation.core.PropKey<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> {
     ctor public OffsetPropKey(String label);
     ctor public OffsetPropKey();
@@ -109,14 +105,14 @@
 
   public final class PropertyKeysKt {
     method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
   }
 
   public final class PxPropKey implements androidx.compose.animation.core.PropKey<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> {
@@ -154,6 +150,7 @@
   }
 
   public final class TransitionKt {
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColor(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.graphics.Color>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.graphics.Color> targetValueByState);
     method @Deprecated @VisibleForTesting public static void setTransitionsEnabled(boolean p);
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.TransitionState transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional String? label, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished);
   }
diff --git a/compose/animation/animation/api/restricted_current.txt b/compose/animation/animation/api/restricted_current.txt
index d9e7a92..3922f7a 100644
--- a/compose/animation/animation/api/restricted_current.txt
+++ b/compose/animation/animation/api/restricted_current.txt
@@ -94,10 +94,6 @@
   @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalAnimationApi {
   }
 
-  public final class LegacyTransitionKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static <T> void Transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionState,kotlin.Unit> children);
-  }
-
   public final class OffsetPropKey implements androidx.compose.animation.core.PropKey<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> {
     ctor public OffsetPropKey(String label);
     ctor public OffsetPropKey();
@@ -109,14 +105,14 @@
 
   public final class PropertyKeysKt {
     method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
-    method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+    method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
   }
 
   public final class PxPropKey implements androidx.compose.animation.core.PropKey<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> {
@@ -154,6 +150,7 @@
   }
 
   public final class TransitionKt {
+    method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColor(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.graphics.Color>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.graphics.Color> targetValueByState);
     method @Deprecated @VisibleForTesting public static void setTransitionsEnabled(boolean p);
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.TransitionState transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional String? label, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished);
   }
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 043cb3a..feac8d4 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -21,6 +21,7 @@
     implementation project(":compose:ui:ui-text")
     implementation project(':compose:animation:animation')
     implementation project(':compose:animation:animation:animation-samples')
+    implementation project(':compose:animation:animation-core:animation-core-samples')
     implementation project(':compose:foundation:foundation')
     implementation project(':compose:material:material')
 }
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt
index d28cb90..2d1b4b4 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt
@@ -27,7 +27,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Button
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -62,13 +62,15 @@
                 Text("Remove")
             }
         }
-        LazyColumnForIndexed(turquoiseColors) { i, color ->
-            AnimatedVisibility(
-                (turquoiseColors.size - itemNum) <= i,
-                enter = expandVertically(),
-                exit = shrinkVertically()
-            ) {
-                Spacer(Modifier.fillMaxWidth().height(90.dp).background(color))
+        LazyColumn {
+            itemsIndexed(turquoiseColors) { i, color ->
+                AnimatedVisibility(
+                    (turquoiseColors.size - itemNum) <= i,
+                    enter = expandVertically(),
+                    exit = shrinkVertically()
+                ) {
+                    Spacer(Modifier.fillMaxWidth().height(90.dp).background(color))
+                }
             }
         }
 
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index 1dbecc6..59b6638 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -26,9 +26,6 @@
             "State Transition Demos",
             listOf(
                 ComposableDemo("Multi-dimensional prop") { MultiDimensionalAnimationDemo() },
-                ComposableDemo("State animation with interruptions") {
-                    StateAnimationWithInterruptionsDemo()
-                },
                 ComposableDemo("State based ripple") { StateBasedRippleDemo() },
                 ComposableDemo("Repeating rotation") { RepeatedRotationDemo() },
                 ComposableDemo("Manual animation clock") { AnimatableSeekBarDemo() },
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt
index d5b30d9..4e85b17 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt
@@ -37,7 +37,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.selection.selectable
 import androidx.compose.material.Button
 import androidx.compose.material.Checkbox
@@ -224,11 +224,10 @@
                     }
                 }
             }
-            LazyColumnFor(
-                menuText,
-                modifier = Modifier.fillMaxSize().background(Color(0xFFd8c7ff))
-            ) {
-                Text(it, Modifier.padding(5.dp))
+            LazyColumn(Modifier.fillMaxSize().background(Color(0xFFd8c7ff))) {
+                items(menuText) {
+                    Text(it, Modifier.padding(5.dp))
+                }
             }
         }
     }
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt
index 734e749..d0801a5 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt
@@ -16,69 +16,10 @@
 
 package androidx.compose.animation.demos
 
-import androidx.compose.animation.ColorPropKey
-import androidx.compose.animation.core.FloatPropKey
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.transitionDefinition
-import androidx.compose.animation.transition
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.animation.core.samples.GestureAnimationSample
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.pressIndicatorGestureFilter
-import androidx.compose.ui.graphics.Color
-
-private const val halfSize = 200f
-
-private enum class ComponentState { Pressed, Released }
-
-private val scale = FloatPropKey()
-private val color = ColorPropKey()
-
-private val definition = transitionDefinition<ComponentState> {
-    state(ComponentState.Released) {
-        this[scale] = 1f
-        this[color] = Color(red = 0, green = 200, blue = 0, alpha = 255)
-    }
-    state(ComponentState.Pressed) {
-        this[scale] = 3f
-        this[color] = Color(red = 0, green = 100, blue = 0, alpha = 255)
-    }
-    transition {
-        scale using spring(
-            stiffness = 50f
-        )
-        color using spring(
-            stiffness = 50f
-        )
-    }
-}
 
 @Composable
 fun GestureBasedAnimationDemo() {
-    val toState = remember { mutableStateOf(ComponentState.Released) }
-    val pressIndicator =
-        Modifier.pressIndicatorGestureFilter(
-            onStart = { toState.value = ComponentState.Pressed },
-            onStop = { toState.value = ComponentState.Released },
-            onCancel = { toState.value = ComponentState.Released }
-        )
-
-    val state = transition(definition = definition, toState = toState.value)
-    ScaledColorRect(pressIndicator, scale = state[scale], color = state[color])
-}
-
-@Composable
-private fun ScaledColorRect(modifier: Modifier = Modifier, scale: Float, color: Color) {
-    Canvas(modifier.fillMaxSize()) {
-        drawRect(
-            color,
-            topLeft = Offset(center.x - halfSize * scale, center.y - halfSize * scale),
-            size = Size(halfSize * 2 * scale, halfSize * 2 * scale)
-        )
-    }
+    GestureAnimationSample()
 }
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
index 0070e96..e0aa04d 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
@@ -16,10 +16,9 @@
 
 package androidx.compose.animation.demos
 
-import androidx.compose.animation.core.AnimationConstants
 import androidx.compose.animation.core.RepeatMode
 import androidx.compose.animation.core.animate
-import androidx.compose.animation.core.repeatable
+import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -42,8 +41,7 @@
         animate(
             initialValue = 1f,
             targetValue = 0f,
-            animationSpec = repeatable(
-                iterations = AnimationConstants.Infinite,
+            animationSpec = infiniteRepeatable(
                 animation = tween(1000),
                 repeatMode = RepeatMode.Reverse
             )
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt
index a54d5b7..eb04f22 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt
@@ -16,18 +16,19 @@
 
 package androidx.compose.animation.demos
 
-import androidx.compose.animation.ColorPropKey
-import androidx.compose.animation.RectPropKey
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.animateRect
 import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.transitionDefinition
 import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
+import androidx.compose.animation.core.updateTransition
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.fillMaxSize
 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.Offset
 import androidx.compose.ui.geometry.Rect
@@ -36,33 +37,42 @@
 
 @Composable
 fun MultiDimensionalAnimationDemo() {
-    val currentState = remember { mutableStateOf(AnimState.Collapsed) }
+    var currentState by remember { mutableStateOf(AnimState.Collapsed) }
     val onClick = {
         // Cycle through states when clicked.
-        currentState.value = when (currentState.value) {
+        currentState = when (currentState) {
             AnimState.Collapsed -> AnimState.Expanded
             AnimState.Expanded -> AnimState.PutAway
             AnimState.PutAway -> AnimState.Collapsed
         }
     }
 
-    val width = remember { mutableStateOf(0f) }
-    val height = remember { mutableStateOf(0f) }
-    val state = transition(
-        definition = remember(width.value, height.value) {
-            createTransDef(width.value, height.value)
-        },
-        toState = currentState.value
-    )
-    Canvas(modifier = Modifier.fillMaxSize().clickable(onClick = onClick, indication = null)) {
-        width.value = size.width
-        height.value = size.height
+    var width by remember { mutableStateOf(0f) }
+    var height by remember { mutableStateOf(0f) }
+    val transition = updateTransition(currentState)
+    val rect by transition.animateRect({ spring(stiffness = 100f) }) {
+        when (it) {
+            AnimState.Collapsed -> Rect(600f, 600f, 900f, 900f)
+            AnimState.Expanded -> Rect(0f, 400f, width, height - 400f)
+            AnimState.PutAway -> Rect(width - 300f, height - 300f, width, height)
+        }
+    }
 
-        val bounds = state[bounds]
+    val color by transition.animateColor(transitionSpec = { tween(durationMillis = 500) }) {
+        when (it) {
+            AnimState.Collapsed -> Color.LightGray
+            AnimState.Expanded -> Color(0xFFd0fff8)
+            AnimState.PutAway -> Color(0xFFe3ffd9)
+        }
+    }
+    Canvas(modifier = Modifier.fillMaxSize().clickable(onClick = onClick, indication = null)) {
+        width = size.width
+        height = size.height
+
         drawRect(
-            state[background],
-            topLeft = Offset(bounds.left, bounds.top),
-            size = Size(bounds.width, bounds.height)
+            color,
+            topLeft = Offset(rect.left, rect.top),
+            size = Size(rect.width, rect.height)
         )
     }
 }
@@ -71,36 +81,4 @@
     Collapsed,
     Expanded,
     PutAway
-}
-
-// Both PropKeys below are multi-dimensional property keys. That means each dimension's
-// value and velocity will be tracked independently. In the case of a color, each color
-// channel is a separate dimension. For rectangles, the dimensions are: top, left,
-// right and bottom.
-private val background = ColorPropKey()
-private val bounds = RectPropKey()
-
-private fun createTransDef(width: Float, height: Float) =
-    transitionDefinition<AnimState> {
-        state(AnimState.Collapsed) {
-            this[background] = Color.LightGray
-            this[bounds] = Rect(600f, 600f, 900f, 900f)
-        }
-        state(AnimState.Expanded) {
-            this[background] = Color(0xFFd0fff8)
-            this[bounds] = Rect(0f, 400f, width, height - 400f)
-        }
-        state(AnimState.PutAway) {
-            this[background] = Color(0xFFe3ffd9)
-            this[bounds] = Rect(width - 300f, height - 300f, width, height)
-        }
-
-        transition {
-            bounds using spring(
-                stiffness = 100f
-            )
-            background using tween(
-                durationMillis = 500
-            )
-        }
-    }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt
index 5917cc2..c365c64 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt
@@ -16,31 +16,32 @@
 
 package androidx.compose.animation.demos
 
-import androidx.compose.animation.core.FloatPropKey
 import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
 import androidx.compose.animation.core.repeatable
-import androidx.compose.animation.core.transitionDefinition
+import androidx.compose.animation.core.updateTransition
 import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.Button
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.rotate
-import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 
 @Composable
 fun RepeatedRotationDemo() {
@@ -50,23 +51,39 @@
             .wrapContentSize(Alignment.Center),
         verticalArrangement = Arrangement.SpaceEvenly
     ) {
-        val textStyle = TextStyle(fontSize = 18.sp)
-        Text(
-            modifier = Modifier.tapGestureFilter(onTap = { state.value = RotationStates.Rotated }),
-            text = "Rotate 10 times",
-            style = textStyle
-        )
-        Text(
-            modifier = Modifier.tapGestureFilter(onTap = { state.value = RotationStates.Original }),
-            text = "Reset",
-            style = textStyle
-        )
-        val transitionState = transition(
-            definition = definition,
-            toState = state.value
-        )
+        Button(
+            { state.value = RotationStates.Rotated }
+        ) {
+            Text(text = "Rotate 10 times")
+        }
+        Spacer(Modifier.height(10.dp))
+        Button(
+            { state.value = RotationStates.Original }
+        ) {
+            Text(text = "Reset")
+        }
+        Spacer(Modifier.height(10.dp))
+        val transition = updateTransition(state.value)
+        val rotation by transition.animateFloat(
+            {
+                if (it.initialState == RotationStates.Original) {
+                    repeatable(
+                        iterations = 10,
+                        animation = keyframes {
+                            durationMillis = 1000
+                            0f at 0 with LinearEasing
+                            360f at 1000
+                        }
+                    )
+                } else {
+                    tween(durationMillis = 300)
+                }
+            }
+        ) {
+            0f
+        }
         Canvas(Modifier.preferredSize(100.dp)) {
-            rotate(transitionState[rotation], Offset.Zero) {
+            rotate(rotation, Offset.Zero) {
                 drawRect(Color(0xFF00FF00))
             }
         }
@@ -76,29 +93,4 @@
 private enum class RotationStates {
     Original,
     Rotated
-}
-
-private val rotation = FloatPropKey()
-
-private val definition = transitionDefinition<RotationStates> {
-    state(RotationStates.Original) {
-        this[rotation] = 0f
-    }
-    state(RotationStates.Rotated) {
-        this[rotation] = 360f
-    }
-    transition(RotationStates.Original to RotationStates.Rotated) {
-        rotation using repeatable(
-            iterations = 10,
-            animation = tween(
-                easing = LinearEasing,
-                durationMillis = 1000
-            )
-        )
-    }
-    transition(RotationStates.Rotated to RotationStates.Original) {
-        rotation using tween(
-            durationMillis = 300
-        )
-    }
-}
+}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
index 9d70c16..933882e 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
@@ -42,7 +42,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.gesture.util.VelocityTracker
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.DrawScope
@@ -54,7 +53,6 @@
 import kotlinx.coroutines.launch
 import kotlin.math.roundToInt
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun SpringBackScrollingDemo() {
     Column(Modifier.fillMaxHeight()) {
@@ -72,12 +70,12 @@
         val gesture = Modifier.pointerInput {
             coroutineScope {
                 while (true) {
-                    val pointerId = handlePointerInput {
+                    val pointerId = awaitPointerEventScope {
                         awaitFirstDown().id
                     }
                     val velocityTracker = VelocityTracker()
                     mutatorMutex.mutate(MutatePriority.UserInput) {
-                        handlePointerInput {
+                        awaitPointerEventScope {
                             horizontalDrag(pointerId) {
                                 scrollPosition += it.positionChange().x
                                 velocityTracker.addPosition(
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateAnimationWithInterruptionsDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateAnimationWithInterruptionsDemo.kt
deleted file mode 100644
index cd5d8f6..0000000
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateAnimationWithInterruptionsDemo.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation.demos
-
-import android.os.Handler
-import android.os.Looper
-import androidx.compose.animation.ColorPropKey
-import androidx.compose.animation.core.FloatPropKey
-import androidx.compose.animation.core.TransitionState
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.transitionDefinition
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-
-@Composable
-fun StateAnimationWithInterruptionsDemo() {
-    Box(Modifier.fillMaxSize()) {
-        ColorRect()
-    }
-}
-
-private val background = ColorPropKey()
-private val y = FloatPropKey()
-
-private enum class OverlayState {
-    Open,
-    Closed
-}
-
-private val definition = transitionDefinition<OverlayState> {
-    state(OverlayState.Open) {
-        this[background] = Color(red = 128, green = 128, blue = 128, alpha = 255)
-        this[y] = 1f // percentage
-    }
-    state(OverlayState.Closed) {
-        this[background] = Color(red = 188, green = 222, blue = 145, alpha = 255)
-        this[y] = 0f // percentage
-    }
-    // Apply this transition to all state changes (i.e. Open -> Closed and Closed -> Open)
-    transition {
-        background using tween(
-            durationMillis = 800
-        )
-        y using spring(
-            // Extremely low stiffness
-            stiffness = 40f
-        )
-    }
-}
-
-private val handler = Handler(Looper.getMainLooper())
-
-@Composable
-private fun ColorRect() {
-    var toState by mutableStateOf(OverlayState.Closed)
-    handler.postDelayed(
-        object : Runnable {
-            override fun run() {
-                if ((0..1).random() == 0) {
-                    toState = OverlayState.Open
-                } else {
-                    toState = OverlayState.Closed
-                }
-            }
-        },
-        (200..800).random().toLong()
-    )
-    val state = transition(definition = definition, toState = toState)
-    ColorRectState(state = state)
-}
-
-@Composable
-private fun ColorRectState(state: TransitionState) {
-    val color = state[background]
-    val scaleY = state[y]
-    Canvas(Modifier.fillMaxSize().background(color = color)) {
-        drawRect(
-            Color(alpha = 255, red = 255, green = 255, blue = 255),
-            topLeft = Offset(100f, 0f),
-            size = Size(size.width - 200f, scaleY * size.height)
-        )
-    }
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt
index 03c4319..917bcd8 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt
@@ -16,26 +16,25 @@
 
 package androidx.compose.animation.demos
 
-import android.graphics.PointF
-import androidx.compose.animation.core.FloatPropKey
-import androidx.compose.animation.core.InterruptionHandling
-import androidx.compose.animation.core.TransitionDefinition
-import androidx.compose.animation.core.TransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateDp
+import androidx.compose.animation.core.animateFloat
 import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.transitionDefinition
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.updateTransition
 import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 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.Offset
 import androidx.compose.ui.gesture.pressIndicatorGestureFilter
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.unit.dp
 
 @Composable
@@ -47,37 +46,76 @@
 
 @Composable
 private fun RippleRect() {
-    val radius = with(AmbientDensity.current) { TargetRadius.toPx() }
-    val toState = remember { mutableStateOf(ButtonStatus.Initial) }
-    val rippleTransDef = remember { createTransDef(radius) }
+    var down by remember { mutableStateOf(Offset(0f, 0f)) }
+    var toState by remember { mutableStateOf(ButtonStatus.Initial) }
     val onPress: (Offset) -> Unit = { position ->
-        down.x = position.x
-        down.y = position.y
-        toState.value = ButtonStatus.Pressed
+        down = position
+        toState = ButtonStatus.Pressed
     }
 
     val onRelease: () -> Unit = {
-        toState.value = ButtonStatus.Released
+        toState = ButtonStatus.Released
     }
-    val state = transition(definition = rippleTransDef, toState = toState.value)
+    val transition = updateTransition(toState)
     RippleRectFromState(
-        Modifier.pressIndicatorGestureFilter(onStart = onPress, onStop = onRelease), state = state
+        Modifier.pressIndicatorGestureFilter(onStart = onPress, onStop = onRelease),
+        center = down,
+        transition = transition
     )
 }
 
 @Composable
-private fun RippleRectFromState(modifier: Modifier = Modifier, state: TransitionState) {
+private fun RippleRectFromState(
+    modifier: Modifier = Modifier,
+    center: Offset,
+    transition: Transition<ButtonStatus>
+) {
+    // TODO: Initial -> Pressed: Uninterruptible
+    // TODO: Pressed -> Released: Uninterruptible
+    // TODO: Auto transition to Initial
+    val alpha by transition.animateFloat(
+        transitionSpec = {
+            if (it.initialState == ButtonStatus.Initial && it.targetState == ButtonStatus.Pressed) {
+                keyframes {
+                    durationMillis = 225
+                    0f at 0 // optional
+                    0.2f at 75
+                    0.2f at 225 // optional
+                }
+            } else if (it.initialState == ButtonStatus.Pressed &&
+                it.targetState == ButtonStatus.Released
+            ) {
+                tween(durationMillis = 220)
+            } else {
+                snap()
+            }
+        }
+    ) {
+        if (it == ButtonStatus.Pressed) 0.2f else 0f
+    }
+
+    val radius by transition.animateDp(
+        transitionSpec = {
+            if (it.initialState == ButtonStatus.Initial && it.targetState == ButtonStatus.Pressed) {
+                tween(225)
+            } else {
+                snap()
+            }
+        }
+    ) {
+        if (it == ButtonStatus.Initial) TargetRadius * 0.3f else TargetRadius + 15.dp
+    }
+
     Canvas(modifier.fillMaxSize()) {
-        // TODO: file bug for when "down" is not a file level val, it's not memoized correctly
         drawCircle(
             Color(
-                alpha = (state[alpha] * 255).toInt(),
+                alpha = (alpha * 255).toInt(),
                 red = 0,
                 green = 235,
                 blue = 224
             ),
-            center = Offset(down.x, down.y),
-            radius = state[radius]
+            center = center,
+            radius = radius.toPx()
         )
     }
 }
@@ -88,49 +126,4 @@
     Released
 }
 
-private val TargetRadius = 200.dp
-
-private val down = PointF(0f, 0f)
-
-private val alpha = FloatPropKey()
-private val radius = FloatPropKey()
-
-private fun createTransDef(targetRadius: Float): TransitionDefinition<ButtonStatus> {
-    return transitionDefinition {
-        state(ButtonStatus.Initial) {
-            this[alpha] = 0f
-            this[radius] = targetRadius * 0.3f
-        }
-        state(ButtonStatus.Pressed) {
-            this[alpha] = 0.2f
-            this[radius] = targetRadius + 15f
-        }
-        state(ButtonStatus.Released) {
-            this[alpha] = 0f
-            this[radius] = targetRadius + 15f
-        }
-
-        // Grow the ripple
-        transition(ButtonStatus.Initial to ButtonStatus.Pressed) {
-            alpha using keyframes {
-                durationMillis = 225
-                0f at 0 // optional
-                0.2f at 75
-                0.2f at 225 // optional
-            }
-            radius using tween(durationMillis = 225)
-            interruptionHandling = InterruptionHandling.UNINTERRUPTIBLE
-        }
-
-        // Fade out the ripple
-        transition(ButtonStatus.Pressed to ButtonStatus.Released) {
-            alpha using tween(durationMillis = 200)
-            interruptionHandling = InterruptionHandling.UNINTERRUPTIBLE
-            // switch back to Initial to prepare for the next ripple cycle
-            nextState = ButtonStatus.Initial
-        }
-
-        // State switch without animation
-        snapTransition(ButtonStatus.Released to ButtonStatus.Initial)
-    }
-}
+private val TargetRadius = 200.dp
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt
index 4045fb6..070ac30 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt
@@ -37,14 +37,14 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun SuspendAnimationDemo() {
     var animStateX by remember {
@@ -59,7 +59,7 @@
         Modifier.fillMaxSize().background(Color(0xffb99aff)).pointerInput {
             coroutineScope {
                 while (true) {
-                    val offset = handlePointerInput {
+                    val offset = awaitPointerEventScope {
                         awaitFirstDown().current.position
                     }
                     val x = offset.x
@@ -86,7 +86,9 @@
     ) {
         Text("Tap anywhere", Modifier.align(Alignment.Center))
         Box(
-            Modifier.offset({ animStateX.value }, { animStateY.value }).size(40.dp)
+            Modifier
+                .offset { IntOffset(animStateX.value.roundToInt(), animStateY.value.roundToInt()) }
+                .size(40.dp)
                 .background(Color(0xff3c1361), CircleShape)
         )
     }
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
index 1e5ef0d..65750ea 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
@@ -42,25 +42,27 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.gesture.util.VelocityTracker
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlin.math.abs
+import kotlin.math.roundToInt
 
 @Composable
 fun SwipeToDismissDemo() {
     Column {
         var index by remember { mutableStateOf(0) }
-        Box(Modifier.height(500.dp).fillMaxWidth()) {
+        val dismissState = remember { DismissState() }
+        Box(Modifier.height(300.dp).fillMaxWidth()) {
             Box(
-                Modifier.swipeToDismiss(index).align(Alignment.BottomCenter).size(300.dp)
+                Modifier.swipeToDismiss(dismissState).align(Alignment.BottomCenter).size(150.dp)
                     .background(pastelColors[index])
             )
         }
@@ -72,6 +74,8 @@
         Button(
             onClick = {
                 index = (index + 1) % pastelColors.size
+                dismissState.alpha = 1f
+                dismissState.offset = 0f
             },
             modifier = Modifier.align(Alignment.CenterHorizontally)
         ) {
@@ -80,33 +84,25 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
-private fun Modifier.swipeToDismiss(index: Int): Modifier = composed {
+private fun Modifier.swipeToDismiss(dismissState: DismissState): Modifier = composed {
     val mutatorMutex = remember { MutatorMutex() }
-    var alpha by remember { mutableStateOf(1f) }
-    var offset by remember { mutableStateOf(0f) }
 
-    remember(index) {
-        // Reset internal states if index has been updated
-        alpha = 1f
-        offset = 0f
-    }
     this.pointerInput {
         fun updateOffset(value: Float) {
-            offset = value
-            alpha = 1f - abs(offset / size.height)
+            dismissState.offset = value
+            dismissState.alpha = 1f - abs(dismissState.offset / size.height)
         }
         coroutineScope {
             while (true) {
-                val pointerId = handlePointerInput {
+                val pointerId = awaitPointerEventScope {
                     awaitFirstDown().id
                 }
                 val velocityTracker = VelocityTracker()
                 // Set a high priority on the mutatorMutex for gestures
                 mutatorMutex.mutate(MutatePriority.UserInput) {
-                    handlePointerInput {
+                    awaitPointerEventScope {
                         verticalDrag(pointerId) {
-                            updateOffset(offset + it.positionChange().y)
+                            updateOffset(dismissState.offset + it.positionChange().y)
                             velocityTracker.addPosition(
                                 it.current.uptime,
                                 it.current.position
@@ -120,9 +116,9 @@
                     // animation job.
                     mutatorMutex.mutate {
                         // Either fling out of the sight, or snap back
-                        val animationState = AnimationState(offset, velocity)
+                        val animationState = AnimationState(dismissState.offset, velocity)
                         val decay = AndroidFlingDecaySpec(this@pointerInput)
-                        if (decay.getTarget(offset, velocity) >= -size.height) {
+                        if (decay.getTarget(dismissState.offset, velocity) >= -size.height) {
                             // Not enough velocity to be dismissed
                             animationState.animateTo(0f) {
                                 updateOffset(value)
@@ -142,7 +138,14 @@
                 }
             }
         }
-    }.offset(y = { offset }).graphicsLayer(alpha = alpha)
+    }
+        .offset { IntOffset(0, dismissState.offset.roundToInt()) }
+        .graphicsLayer(alpha = dismissState.alpha)
+}
+
+private class DismissState {
+    var alpha by mutableStateOf(1f)
+    var offset by mutableStateOf(0f)
 }
 
 internal val pastelColors = listOf(
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt
index 8586b3c..e1e0cff 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.animation.core.FloatSpringSpec
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Box
@@ -338,14 +339,14 @@
         var boundsValue = Bounds(0.dp, 0.dp, 0.dp, 0.dp)
 
         val specForFloat = FloatSpringSpec(visibilityThreshold = 0.01f)
-        val specForVector = FloatSpringSpec(visibilityThreshold = PxVisibilityThreshold)
-        val specForOffset = FloatSpringSpec(visibilityThreshold = PxVisibilityThreshold)
-        val specForBounds = FloatSpringSpec(visibilityThreshold = DpVisibilityThreshold)
+        val specForVector = FloatSpringSpec(visibilityThreshold = 0.5f)
+        val specForOffset = FloatSpringSpec(visibilityThreshold = 0.5f)
+        val specForBounds = FloatSpringSpec(visibilityThreshold = 0.1f)
 
         val content: @Composable (Boolean) -> Unit = { enabled ->
             vectorValue = animate(
                 if (enabled) AnimationVector(100f) else AnimationVector(0f),
-                visibilityThreshold = AnimationVector(PxVisibilityThreshold)
+                visibilityThreshold = AnimationVector(0.5f)
             )
 
             offsetValue = animate(
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
index 9b7b320..7772778 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
@@ -25,8 +25,8 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
index 132dfda..e870d7c 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
@@ -21,6 +21,7 @@
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.AnimationVector2D
 import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.spring
 import androidx.compose.runtime.remember
 import androidx.compose.ui.layout.LayoutModifier
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
index 795d9a5..3f4b536 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
@@ -20,6 +20,7 @@
 import androidx.compose.animation.core.AnimationEndReason
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.spring
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LegacyTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LegacyTransition.kt
deleted file mode 100644
index fcfad1b..0000000
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LegacyTransition.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation
-
-import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.TransitionDefinition
-import androidx.compose.animation.core.TransitionState
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.AmbientAnimationClock
-
-/**
- * __Deprecated:__ [Transition] has been deprecated. Please use [transition] instead.
- *
- * [Transition] composable creates a state-based transition using the animation configuration
- * defined in [TransitionDefinition]. This can be especially useful when animating multiple
- * values from a predefined set of values to another. For animating a single value, consider using
- * [animatedValue], [animatedFloat], [animatedColor] or the more light-weight [animate] APIs.
- *
- * [Transition] starts a new animation or changes the on-going animation when the [toState]
- * parameter is changed to a different value. It dutifully ensures that the animation will head
- * towards new [toState] regardless of what state (or in-between state) it’s currently in: If the
- * transition is not currently animating, having a new [toState] value will start a new animation,
- * otherwise the in-flight animation will correct course and animate towards the new [toState]
- * based on the interruption handling logic.
- *
- * [Transition] takes a transition definition, a target state and child composables.
- * These child composables will be receiving a [TransitionState] object as an argument, which
- * captures all the current values of the animation. Child composables should read the animation
- * values from the [TransitionState] object, and apply the value wherever necessary.
- *
- * @sample androidx.compose.animation.samples.TransitionSample
- *
- * @param definition Transition definition that defines states and transitions
- * @param toState New state to transition to
- * @param clock Optional animation clock that pulses animations when time changes. By default,
- *              the system uses a choreographer based clock read from the [AnimationClockAmbient].
- *              A custom implementation of the [AnimationClockObservable] (such as a
- *              [androidx.compose.animation.core.ManualAnimationClock]) can be supplied here if there’s a need to
- *              manually control the clock (for example in tests).
- * @param initState Optional initial state for the transition. When undefined, the initial state
- *                  will be set to the first [toState] seen in the transition.
- * @param onStateChangeFinished An optional listener to get notified when state change animation
- *                              has completed
- * @param children The children composables that will be animated
- *
- * @see [TransitionDefinition]
- */
-@Deprecated(
-    "Transition has been renamed to transition, which returns a TransitionState instead " +
-        "of passing it to children",
-    replaceWith = ReplaceWith(
-        "transition(definition, toState, clock, initState, null, onStateChangeFinished)",
-        "androidx.compose.animation.transition"
-    )
-)
-@Composable
-fun <T> Transition(
-    definition: TransitionDefinition<T>,
-    toState: T,
-    clock: AnimationClockObservable = AmbientAnimationClock.current,
-    initState: T = toState,
-    onStateChangeFinished: ((T) -> Unit)? = null,
-    children: @Composable (state: TransitionState) -> Unit
-) {
-    val state = transition(definition, toState, clock, initState, null, onStateChangeFinished)
-    children(state)
-}
\ No newline at end of file
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt
index bfc1b0d..12cc147 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt
@@ -204,47 +204,55 @@
 /**
  * A type converter that converts a [Rect] to a [AnimationVector4D], and vice versa.
  */
+@Deprecated("Rect.VectorConverter has been moved to animation-core library")
 val Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>
     get() = RectToVector
 
 /**
  * A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
  */
+@Deprecated("Dp.VectorConverter has been moved to animation-core library")
 val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
     get() = DpToVector
 
 /**
  * A type converter that converts a [Position] to a [AnimationVector2D], and vice versa.
  */
+@Deprecated("Position.VectorConverter has been moved to animation-core library")
 val Position.Companion.VectorConverter: TwoWayConverter<Position, AnimationVector2D>
     get() = PositionToVector
 
 /**
  * A type converter that converts a [Size] to a [AnimationVector2D], and vice versa.
  */
+@Deprecated("Size.VectorConverter has been moved to animation-core library")
 val Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>
     get() = SizeToVector
 
 /**
  * A type converter that converts a [Bounds] to a [AnimationVector4D], and vice versa.
  */
+@Deprecated("Bounds.VectorConverter has been moved to animation-core library")
 val Bounds.Companion.VectorConverter: TwoWayConverter<Bounds, AnimationVector4D>
     get() = BoundsToVector
 
 /**
  * A type converter that converts a [Offset] to a [AnimationVector2D], and vice versa.
  */
+@Deprecated("Offset.VectorConverter has been moved to animation-core library")
 val Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>
     get() = OffsetToVector
 
 /**
  * A type converter that converts a [IntOffset] to a [AnimationVector2D], and vice versa.
  */
+@Deprecated("IntOffset.VectorConverter has been moved to animation-core library")
 val IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>
     get() = IntOffsetToVector
 
 /**
  * A type converter that converts a [IntSize] to a [AnimationVector2D], and vice versa.
  */
+@Deprecated("IntSize.VectorConverter has been moved to animation-core library")
 val IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>
     get() = IntSizeToVector
\ No newline at end of file
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt
index 5fd5dab..84ae2d8 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt
@@ -21,10 +21,10 @@
 import androidx.compose.animation.core.AnimationState
 import androidx.compose.animation.core.AnimationVector
 import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.AnimationVector4D
 import androidx.compose.animation.core.SpringSpec
 import androidx.compose.animation.core.TwoWayConverter
 import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
 import androidx.compose.animation.core.animateTo
 import androidx.compose.animation.core.isFinished
 import androidx.compose.runtime.Composable
@@ -45,26 +45,6 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Position
-import androidx.compose.ui.unit.dp
-
-internal const val DpVisibilityThreshold = 0.1f
-internal const val PxVisibilityThreshold = 0.5f
-
-// Dp-based visibility threshold
-private val DpVisibilityThreshold4D = AnimationVector4D(
-    DpVisibilityThreshold,
-    DpVisibilityThreshold,
-    DpVisibilityThreshold,
-    DpVisibilityThreshold
-)
-
-// Px-based visibility threshold
-private val PxVisibilityThreshold4D = AnimationVector4D(
-    PxVisibilityThreshold,
-    PxVisibilityThreshold,
-    PxVisibilityThreshold,
-    PxVisibilityThreshold
-)
 
 private val defaultAnimation = SpringSpec<Float>()
 
@@ -159,7 +139,7 @@
 fun animate(
     target: Dp,
     animSpec: AnimationSpec<Dp> = remember {
-        SpringSpec(visibilityThreshold = DpVisibilityThreshold.dp)
+        SpringSpec(visibilityThreshold = Dp.VisibilityThreshold)
     },
     endListener: ((Dp) -> Unit)? = null
 ): Dp {
@@ -187,7 +167,7 @@
     target: Position,
     animSpec: AnimationSpec<Position> = remember {
         SpringSpec(
-            visibilityThreshold = Position(DpVisibilityThreshold.dp, DpVisibilityThreshold.dp)
+            visibilityThreshold = Position.VisibilityThreshold
         )
     },
     endListener: ((Position) -> Unit)? = null
@@ -217,7 +197,7 @@
 fun animate(
     target: Size,
     animSpec: AnimationSpec<Size> = remember {
-        SpringSpec(visibilityThreshold = Size(PxVisibilityThreshold, PxVisibilityThreshold))
+        SpringSpec(visibilityThreshold = Size.VisibilityThreshold)
     },
     endListener: ((Size) -> Unit)? = null
 ): Size {
@@ -244,10 +224,7 @@
 fun animate(
     target: Bounds,
     animSpec: AnimationSpec<Bounds> = remember {
-        SpringSpec(
-            visibilityThreshold = Bounds.VectorConverter.convertFromVector
-            (DpVisibilityThreshold4D)
-        )
+        SpringSpec(visibilityThreshold = Bounds.VisibilityThreshold)
     },
     endListener: ((Bounds) -> Unit)? = null
 ): Bounds {
@@ -278,7 +255,7 @@
 fun animate(
     target: Offset,
     animSpec: AnimationSpec<Offset> = remember {
-        SpringSpec(visibilityThreshold = Offset(PxVisibilityThreshold, PxVisibilityThreshold))
+        SpringSpec(visibilityThreshold = Offset.VisibilityThreshold)
     },
     endListener: ((Offset) -> Unit)? = null
 ): Offset {
@@ -307,10 +284,7 @@
 fun animate(
     target: Rect,
     animSpec: AnimationSpec<Rect> = remember {
-        SpringSpec(
-            visibilityThreshold =
-                Rect.VectorConverter.convertFromVector(PxVisibilityThreshold4D)
-        )
+        SpringSpec(visibilityThreshold = Rect.VisibilityThreshold)
     },
     endListener: ((Rect) -> Unit)? = null
 ): Rect {
@@ -364,7 +338,7 @@
 fun animate(
     target: IntOffset,
     animSpec: AnimationSpec<IntOffset> = remember {
-        SpringSpec(visibilityThreshold = IntOffset(1, 1))
+        SpringSpec(visibilityThreshold = IntOffset.VisibilityThreshold)
     },
     endListener: ((IntOffset) -> Unit)? = null
 ): IntOffset {
@@ -390,7 +364,7 @@
 fun animate(
     target: IntSize,
     animSpec: AnimationSpec<IntSize> = remember {
-        SpringSpec(visibilityThreshold = IntSize(1, 1))
+        SpringSpec(visibilityThreshold = IntSize.VisibilityThreshold)
     },
     endListener: ((IntSize) -> Unit)? = null
 ): IntSize {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt
index cb8442f..7ae0bb2 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt
@@ -18,18 +18,28 @@
 
 import androidx.compose.animation.core.AnimationClockObservable
 import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.FiniteAnimationSpec
 import androidx.compose.animation.core.InternalAnimationApi
 import androidx.compose.animation.core.PropKey
+import androidx.compose.animation.core.Transition
 import androidx.compose.animation.core.TransitionAnimation
 import androidx.compose.animation.core.TransitionDefinition
 import androidx.compose.animation.core.TransitionState
+import androidx.compose.animation.core.animateValue
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.repeatable
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.onCommit
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.util.annotation.VisibleForTesting
 
@@ -130,6 +140,7 @@
 ) : TransitionState {
 
     private var animationPulse by mutableStateOf(0L)
+
     @InternalAnimationApi
     val anim: TransitionAnimation<T> =
         TransitionAnimation(transitionDef, clock, initState, label).apply {
@@ -144,4 +155,42 @@
         val pulse = animationPulse
         return anim[propKey]
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Creates a [Color] animation as a part of the given [Transition]. This means the lifecycle
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ *
+ * @see animateValue
+ * @see androidx.compose.animation.core.animateFloat
+ * @see androidx.compose.animation.core.Transition
+ * @see androidx.compose.animation.core.updateTransition
+ */
+@Composable
+inline fun <S> Transition<S>.animateColor(
+    noinline transitionSpec:
+        @Composable (states: Transition.States<S>) -> FiniteAnimationSpec<Color> = { spring() },
+    targetValueByState: @Composable (state: S) -> Color
+): State<Color> {
+    val colorSpace = targetValueByState(targetState).colorSpace
+    val typeConverter = remember(colorSpace) {
+        Color.VectorConverter(colorSpace)
+    }
+
+    return animateValue(typeConverter, transitionSpec, targetValueByState)
+}
diff --git a/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt b/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
index cf2131d..49a60a3 100644
--- a/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
+++ b/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.animation.core.AnimationVector1D
 import androidx.compose.animation.core.AnimationVector2D
 import androidx.compose.animation.core.AnimationVector4D
+import androidx.compose.animation.core.VectorConverter
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
diff --git a/compose/compiler/compiler-hosted/integration-tests/lint-baseline.xml b/compose/compiler/compiler-hosted/integration-tests/lint-baseline.xml
deleted file mode 100644
index db82975..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/lint-baseline.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
-    <issue
-        id="IgnoreWithoutReason"
-        message="Test is ignored without giving any explanation"
-        errorLine1="    @Ignore"
-        errorLine2="    ~~~~~~~">
-        <location
-            file="src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt"
-            line="41"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="IgnoreWithoutReason"
-        message="Test is ignored without giving any explanation"
-        errorLine1="    @Ignore"
-        errorLine2="    ~~~~~~~">
-        <location
-            file="src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt"
-            line="63"
-            column="5"/>
-    </issue>
-
-</issues>
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
index 7e58ce0..c21c5e6 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
@@ -126,7 +126,6 @@
                 fun makeComposer(): Composer<*> {
                     val container = LinearLayout(__context!!)
                     return Composer(
-                        SlotTable(),
                         UiApplier(container),
                         Recomposer.current()
                     )
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index de2790c..6dcaf71 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -68,11 +68,10 @@
 abstract class ComposeIrTransformTest : AbstractIrTransformTest() {
     open val liveLiteralsEnabled get() = false
     open val sourceInformationEnabled get() = true
-    open val intrinsicRememberEnabled get() = false
     private val extension = ComposeIrGenerationExtension(
         liveLiteralsEnabled,
         sourceInformationEnabled,
-        intrinsicRememberEnabled
+        intrinsicRememberEnabled = true
     )
     // Some tests require the plugin context in order to perform assertions, for example, a
     // context is required to determine the stability of a type using the StabilityInferencer.
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
index 0cd4d99..b0eed19 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import android.view.View
 import androidx.compose.runtime.Composer
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.snapshots.Snapshot
@@ -42,9 +41,6 @@
         )
     }
 
-    @Suppress("UNCHECKED_CAST")
-    fun View.getComposedSet(tagId: Int): Set<String>? = getTag(tagId) as? Set<String>
-
     @OptIn(ExperimentalComposeApi::class)
     protected fun execute(block: () -> Unit) {
         val scheduler = RuntimeEnvironment.getMasterScheduler()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt
new file mode 100644
index 0000000..bacb9b6
--- /dev/null
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.compiler.plugins.kotlin
+
+import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.junit.Test
+
+class CodegenMetadataTests : AbstractLoweringTests() {
+
+    override fun updateConfiguration(configuration: CompilerConfiguration) {
+        super.updateConfiguration(configuration)
+        configuration.put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
+    }
+
+    @Test
+    fun testBasicFunctionality(): Unit = ensureSetup {
+        val className = "Test_${uniqueNumber++}"
+        val fileName = "$className.kt"
+        val loader = classLoader(
+            """
+            import kotlin.reflect.full.primaryConstructor
+            import kotlin.reflect.jvm.isAccessible
+            data class MyClass(val someBoolean: Boolean? = false)
+            object Main { @JvmStatic fun main() { MyClass::class.java.kotlin.primaryConstructor!!.isAccessible = true } }
+            """,
+            fileName,
+            true
+        )
+        val main = loader.loadClass("Main").methods.single { it.name == "main" }
+        main.invoke(null)
+    }
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
index bf3a820..9a43019 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import android.view.View
 import android.widget.Button
 import android.widget.LinearLayout
 import android.widget.TextView
@@ -179,13 +180,13 @@
             """
             import androidx.compose.runtime.*
 
-            @Composable val foo get() = 123
+            val foo @Composable get() = 123
 
             class A {
-                @Composable val bar get() = 123
+                val bar @Composable get() = 123
             }
 
-            @Composable val A.bam get() = 123
+            val A.bam @Composable get() = 123
 
             @Composable fun Foo() {
             }
@@ -260,13 +261,13 @@
     fun testPropertyValues(): Unit = ensureSetup {
         compose(
             """
-            @Composable val foo get() = "123"
+            val foo @Composable get() = "123"
 
             class A {
-                @Composable val bar get() = "123"
+                val bar @Composable get() = "123"
             }
 
-            @Composable val A.bam get() = "123"
+            val A.bam @Composable get() = "123"
 
             @Composable
             fun App() {
@@ -2728,6 +2729,9 @@
     }
 }
 
+@Suppress("UNCHECKED_CAST")
+fun View.getComposedSet(tagId: Int): Set<String>? = getTag(tagId) as? Set<String>
+
 private val noParameters = { emptyMap<String, String>() }
 
 private inline fun <reified T : PsiElement> PsiElement.parentOfType(): T? = parentOfType(T::class)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
index e6b6625..ba815cf 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
@@ -34,13 +34,13 @@
         """
             import androidx.compose.runtime.*
 
-            @Composable val foo get() = 123
+            val foo @Composable get() = 123
 
             class A {
-                @Composable val bar get() = 123
+                val bar @Composable get() = 123
             }
 
-            @Composable val A.bam get() = 123
+            val A.bam @Composable get() = 123
 
             @Composable
             fun test() {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
index ef97c5e..fa1abfc 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
@@ -855,12 +855,11 @@
     @Test
     fun testComposableTopLevelProperty(): Unit = checkApi(
         """
-            @Composable val foo: Int get() { return 123 }
+            val foo: Int @Composable get() { return 123 }
         """,
         """
             public final class TestKt {
               public final static getFoo(Landroidx/compose/runtime/Composer;I)I
-              public static synthetic getFoo%annotations()V
             }
         """
     )
@@ -869,14 +868,13 @@
     fun testComposableProperty(): Unit = checkApi(
         """
             class Foo {
-                @Composable val foo: Int get() { return 123 }
+                val foo: Int @Composable get() { return 123 }
             }
         """,
         """
             public final class Foo {
               public <init>()V
               public final getFoo(Landroidx/compose/runtime/Composer;I)I
-              public static synthetic getFoo%annotations()V
               public final static I %stable
               static <clinit>()V
             }
@@ -942,7 +940,7 @@
     @Test
     fun testCallingProperties(): Unit = checkApi(
         """
-            @Composable val bar: Int get() { return 123 }
+            val bar: Int @Composable get() { return 123 }
 
             @Composable fun Example() {
                 bar
@@ -951,7 +949,6 @@
         """
             public final class TestKt {
               public final static getBar(Landroidx/compose/runtime/Composer;I)I
-              public static synthetic getBar%annotations()V
               final static INNERCLASS TestKt%Example%1 null null
               public final static Example(Landroidx/compose/runtime/Composer;I)V
             }
@@ -1201,8 +1198,7 @@
     @Test
     fun testDexNaming(): Unit = checkApi(
         """
-            @Composable
-            val myProperty: () -> Unit get() {
+            val myProperty: () -> Unit @Composable get() {
                 return {  }
             }
         """,
@@ -1210,7 +1206,6 @@
             public final class TestKt {
               final static INNERCLASS TestKt%myProperty%1 null null
               public final static getMyProperty(Landroidx/compose/runtime/Composer;I)Lkotlin/jvm/functions/Function0;
-              public static synthetic getMyProperty%annotations()V
             }
             final class TestKt%myProperty%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
               <init>()V
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index 07fa0354..6228e46 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -55,7 +55,7 @@
     @Test
     fun testCallingProperties(): Unit = composerParam(
         """
-            @Composable val bar: Int get() { return 123 }
+            val bar: Int @Composable get() { return 123 }
 
             @ComposableContract(restartable = false) @Composable fun Example() {
                 bar
@@ -289,8 +289,7 @@
     @Test
     fun testDexNaming(): Unit = composerParam(
         """
-            @Composable
-            val myProperty: () -> Unit get() {
+            val myProperty: () -> Unit @Composable get() {
                 return {  }
             }
         """,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
index 30e6dbf..93459df 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
@@ -447,7 +447,7 @@
                     import androidx.compose.runtime.Composable
 
                     class Foo {
-                      @Composable val value: Int get() = 123
+                      val value: Int @Composable get() = 123
                     }
                  """
                 ),
@@ -687,7 +687,7 @@
 
                     import androidx.compose.runtime.*
 
-                    @Composable val foo: Int get() { return 123 }
+                    val foo: Int @Composable get() { return 123 }
                  """
                 ),
                 "Main" to mapOf(
@@ -740,6 +740,35 @@
     }
 
     @Test
+    fun testXModuleComposableProperty(): Unit = ensureSetup {
+        compile(
+            mapOf(
+                "library module" to mapOf(
+                    "a/Foo.kt" to """
+                    package a
+
+                    import androidx.compose.runtime.*
+
+                    val foo: () -> Unit
+                        @Composable get() = {}
+                 """
+                ),
+                "Main" to mapOf(
+                    "B.kt" to """
+                    import a.foo
+                    import androidx.compose.runtime.*
+
+                    @Composable fun Example() {
+                        val bar = foo
+                        bar()
+                    }
+                """
+                )
+            )
+        )
+    }
+
+    @Test
     fun testXModuleCtorComposableParam(): Unit = ensureSetup {
         compile(
             mapOf(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
index 800bd34..9e9ab11 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
@@ -38,7 +38,7 @@
         configuration.put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
     }
 
-    @Ignore
+    @Ignore("Live literals are currently disabled by default")
     @Test
     fun testBasicFunctionality(): Unit = ensureSetup {
         compose(
@@ -60,7 +60,7 @@
         }
     }
 
-    @Ignore
+    @Ignore("Live literals are currently disabled by default")
     @Test
     fun testObjectFieldsLoweredToStaticFields(): Unit = ensureSetup {
         validateBytecode(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 524d05e..161a44a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -19,7 +19,6 @@
 import org.junit.Test
 
 class RememberIntrinsicTransformTests : ComposeIrTransformTest() {
-    override val intrinsicRememberEnabled: Boolean get() = true
     private fun comparisonPropagation(
         unchecked: String,
         checked: String,
@@ -42,6 +41,214 @@
     )
 
     @Test
+    fun testElidedRememberInsideIfDeoptsRememberAfterIf(): Unit = comparisonPropagation(
+        "",
+        """
+            import androidx.compose.runtime.ComposableContract
+
+            @Composable
+            @ComposableContract(restartable = false)
+            fun app(x: Boolean) {
+                val a = if (x) { remember { 1 } } else { 2 }
+                val b = remember { 2 }
+            }
+        """,
+        """
+            @Composable
+            @ComposableContract(restartable = false)
+            fun app(x: Boolean, %composer: Composer<*>?, %changed: Int) {
+              %composer.startReplaceableGroup(<>, "C(app)<rememb...>:Test.kt")
+              val a = if (x) {
+                %composer.startReplaceableGroup(<>)
+                val tmp0_group = %composer.cache(false) {
+                  val tmp0_return = 1
+                  tmp0_return
+                }
+                %composer.endReplaceableGroup()
+                tmp0_group
+              } else {
+                %composer.startReplaceableGroup(<>)
+                %composer.endReplaceableGroup()
+                2
+              }
+              val b = remember({
+                val tmp0_return = 2
+                tmp0_return
+              }, %composer, 0)
+              %composer.endReplaceableGroup()
+            }
+        """
+    )
+
+    @Test
+    fun testMultipleParamInputs(): Unit = comparisonPropagation(
+        """
+        """,
+        """
+            @Composable
+            fun <T> loadResourceInternal(
+                key: String,
+                pendingResource: T? = null,
+                failedResource: T? = null
+            ): Boolean {
+                val deferred = remember(key, pendingResource, failedResource) {
+                    123
+                }
+                return deferred > 10
+            }
+        """,
+        """
+            @Composable
+            fun <T> loadResourceInternal(key: String, pendingResource: T?, failedResource: T?, %composer: Composer<*>?, %changed: Int, %default: Int): Boolean {
+              %composer.startReplaceableGroup(<>, "C(loadResourceInternal)P(1,2):Test.kt")
+              val pendingResource = if (%default and 0b0010 !== 0) null else pendingResource
+              val failedResource = if (%default and 0b0100 !== 0) null else failedResource
+              val deferred = %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(key) || %changed and 0b0110 === 0b0100 or %changed and 0b01110000 xor 0b00110000 > 32 && %composer.changed(pendingResource) || %changed and 0b00110000 === 0b00100000 or %changed and 0b001110000000 xor 0b000110000000 > 256 && %composer.changed(failedResource) || %changed and 0b000110000000 === 0b000100000000) {
+                val tmp0_return = 123
+                tmp0_return
+              }
+              val tmp0 = deferred > 10
+              %composer.endReplaceableGroup()
+              return tmp0
+            }
+        """
+    )
+
+    @Test
+    fun testRestartableParameterInputsStableUnstableUncertain(): Unit = comparisonPropagation(
+        """
+            class KnownStable
+            class KnownUnstable(var x: Int)
+            interface Uncertain
+        """,
+        """
+            @Composable
+            fun test1(x: KnownStable) {
+                remember(x) { 1 }
+            }
+            @Composable
+            fun test2(x: KnownUnstable) {
+                remember(x) { 1 }
+            }
+            @Composable
+            fun test3(x: Uncertain) {
+                remember(x) { 1 }
+            }
+        """,
+        """
+            @Composable
+            fun test1(x: KnownStable, %composer: Composer<*>?, %changed: Int) {
+              %composer.startRestartGroup(<>, "C(test1):Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                %composer.cache(%dirty and 0b1110 === 0b0100) {
+                  val tmp0_return = 1
+                  tmp0_return
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer<*>?, %force: Int ->
+                test1(x, %composer, %changed or 0b0001)
+              }
+            }
+            @Composable
+            fun test2(x: KnownUnstable, %composer: Composer<*>?, %changed: Int) {
+              %composer.startRestartGroup(<>, "C(test2):Test.kt")
+              %composer.cache(%composer.changed(x)) {
+                val tmp0_return = 1
+                tmp0_return
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer<*>?, %force: Int ->
+                test2(x, %composer, %changed or 0b0001)
+              }
+            }
+            @Composable
+            fun test3(x: Uncertain, %composer: Composer<*>?, %changed: Int) {
+              %composer.startRestartGroup(<>, "C(test3):Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                %composer.cache(%dirty and 0b1110 === 0b0100 || %dirty and 0b1000 !== 0 && %composer.changed(x)) {
+                  val tmp0_return = 1
+                  tmp0_return
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer<*>?, %force: Int ->
+                test3(x, %composer, %changed or 0b0001)
+              }
+            }
+        """
+    )
+
+    @Test
+    fun testNonRestartableParameterInputsStableUnstableUncertain(): Unit = comparisonPropagation(
+        """
+            class KnownStable
+            class KnownUnstable(var x: Int)
+            interface Uncertain
+        """,
+        """
+            import androidx.compose.runtime.ComposableContract
+
+            @Composable
+            @ComposableContract(restartable=false)
+            fun test1(x: KnownStable) {
+                remember(x) { 1 }
+            }
+            @Composable
+            @ComposableContract(restartable=false)
+            fun test2(x: KnownUnstable) {
+                remember(x) { 1 }
+            }
+            @Composable
+            @ComposableContract(restartable=false)
+            fun test3(x: Uncertain) {
+                remember(x) { 1 }
+            }
+        """,
+        """
+            @Composable
+            @ComposableContract(restartable = false)
+            fun test1(x: KnownStable, %composer: Composer<*>?, %changed: Int) {
+              %composer.startReplaceableGroup(<>, "C(test1):Test.kt")
+              %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(x) || %changed and 0b0110 === 0b0100) {
+                val tmp0_return = 1
+                tmp0_return
+              }
+              %composer.endReplaceableGroup()
+            }
+            @Composable
+            @ComposableContract(restartable = false)
+            fun test2(x: KnownUnstable, %composer: Composer<*>?, %changed: Int) {
+              %composer.startReplaceableGroup(<>, "C(test2):Test.kt")
+              %composer.cache(%composer.changed(x)) {
+                val tmp0_return = 1
+                tmp0_return
+              }
+              %composer.endReplaceableGroup()
+            }
+            @Composable
+            @ComposableContract(restartable = false)
+            fun test3(x: Uncertain, %composer: Composer<*>?, %changed: Int) {
+              %composer.startReplaceableGroup(<>, "C(test3):Test.kt")
+              %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(x) || %changed and 0b0110 === 0b0100) {
+                val tmp0_return = 1
+                tmp0_return
+              }
+              %composer.endReplaceableGroup()
+            }
+        """
+    )
+
+    @Test
     fun testPassedArgs(): Unit = comparisonPropagation(
         """
             class Foo(val a: Int, val b: Int)
@@ -54,7 +261,7 @@
             @Composable
             fun rememberFoo(a: Int, b: Int, %composer: Composer<*>?, %changed: Int): Foo {
               %composer.startReplaceableGroup(<>, "C(rememberFoo):Test.kt")
-              val tmp0 = %composer.cache(%changed and 0b0110 === 0 && %composer.changed(a) || %changed and 0b1110 === 0b0100 or %changed and 0b00110000 === 0 && %composer.changed(b) || %changed and 0b01110000 === 0b00100000) {
+              val tmp0 = %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(a) || %changed and 0b0110 === 0b0100 or %changed and 0b01110000 xor 0b00110000 > 32 && %composer.changed(b) || %changed and 0b00110000 === 0b00100000) {
                 val tmp0_return = Foo(a, b)
                 tmp0_return
               }
@@ -676,7 +883,7 @@
             fun Test(a: Int, %composer: Composer<*>?, %changed: Int): Foo {
               %composer.startReplaceableGroup(<>, "C(Test):Test.kt")
               val b = someInt()
-              val tmp0 = %composer.cache(%changed and 0b0110 === 0 && %composer.changed(a) || %changed and 0b1110 === 0b0100 or %composer.changed(b)) {
+              val tmp0 = %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(a) || %changed and 0b0110 === 0b0100 or %composer.changed(b)) {
                 val tmp0_return = Foo(a, b)
                 tmp0_return
               }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
index b05df3c..3b124e8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
@@ -140,7 +140,7 @@
         """
         import androidx.compose.runtime.*
         @Composable fun C(): Int { return 123 }
-        @Composable val cProp: Int get() = C()
+        val cProp: Int @Composable get() = C()
     """
     )
 
@@ -157,7 +157,7 @@
         import androidx.compose.runtime.*
         @Composable fun C(): Int { return 123 }
         val ncProp: Int = <!COMPOSABLE_INVOCATION!>C<!>()
-        @Composable val <!COMPOSABLE_PROPERTY_BACKING_FIELD!>cProp<!>: Int = <!COMPOSABLE_INVOCATION!>C<!>()
+        @Composable val <!COMPOSABLE_PROPERTY_BACKING_FIELD,DEPRECATED_COMPOSABLE_PROPERTY!>cProp<!>: Int = <!COMPOSABLE_INVOCATION!>C<!>()
     """
     )
 
@@ -868,7 +868,7 @@
             """
             import androidx.compose.runtime.*;
 
-            @Composable val foo: Int get() = 123
+            val foo: Int @Composable get() = 123
 
             fun <!COMPOSABLE_EXPECTED!>App<!>() {
                 <!COMPOSABLE_INVOCATION!>foo<!>
@@ -879,7 +879,7 @@
             """
             import androidx.compose.runtime.*;
 
-            @Composable val foo: Int get() = 123
+            val foo: Int @Composable  get() = 123
 
             @Composable
             fun App() {
@@ -927,10 +927,10 @@
             import androidx.compose.runtime.*;
 
             class A {
-                @Composable val bar get() = 123
+                val bar @Composable get() = 123
             }
 
-            @Composable val A.bam get() = 123
+            val A.bam @Composable get() = 123
 
             @Composable
             fun App() {
@@ -966,7 +966,7 @@
 
             @Composable fun Foo() {}
 
-            @Composable val bam: Int get() {
+            val bam: Int @Composable get() {
                 Foo()
                 return 123
             }
@@ -1003,7 +1003,7 @@
                 val x = object {
                   val <!COMPOSABLE_EXPECTED!>a<!> get() =
                   <!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(2) }
-                  @Composable val c get() = remember { mutableStateOf(4) }
+                  val c @Composable get() = remember { mutableStateOf(4) }
                   @Composable fun bar() { Foo() }
                   fun <!COMPOSABLE_EXPECTED!>foo<!>() {
                     <!COMPOSABLE_INVOCATION!>Foo<!>()
@@ -1012,7 +1012,7 @@
                 class Bar {
                   val <!COMPOSABLE_EXPECTED!>b<!> get() =
                   <!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(6) }
-                  @Composable val c get() = remember { mutableStateOf(7) }
+                  val c @Composable get() = remember { mutableStateOf(7) }
                 }
                 fun <!COMPOSABLE_EXPECTED!>Bam<!>() {
                     <!COMPOSABLE_INVOCATION!>Foo<!>()
@@ -1036,7 +1036,7 @@
             @Composable fun App() {
                 val x = object {
                   val <!COMPOSABLE_EXPECTED!>a<!> get() = <!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(2) }
-                  @Composable val c get() = remember { mutableStateOf(4) }
+                  val c @Composable get() = remember { mutableStateOf(4) }
                   fun <!COMPOSABLE_EXPECTED!>foo<!>() {
                     <!COMPOSABLE_INVOCATION!>Foo<!>()
                   }
@@ -1044,7 +1044,7 @@
                 }
                 class Bar {
                   val <!COMPOSABLE_EXPECTED!>b<!> get() = <!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(6) }
-                  @Composable val c get() = remember { mutableStateOf(7) }
+                  val c @Composable get() = remember { mutableStateOf(7) }
                 }
                 fun <!COMPOSABLE_EXPECTED!>Bam<!>() {
                     <!COMPOSABLE_INVOCATION!>Foo<!>()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
index c63dffc..4463850 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
@@ -29,7 +29,10 @@
             import androidx.compose.runtime.Composable
 
             @Composable
-            val <!COMPOSABLE_PROPERTY_BACKING_FIELD!>foo<!>: Int = 123
+            val <!DEPRECATED_COMPOSABLE_PROPERTY,COMPOSABLE_PROPERTY_BACKING_FIELD!>foo<!>: Int = 123
+
+            val <!COMPOSABLE_PROPERTY_BACKING_FIELD!>bar<!>: Int = 123
+                @Composable get() = field
         """
         )
     }
@@ -78,7 +81,9 @@
             import androidx.compose.runtime.Composable
 
             @Composable
-            val bar: Int get() = 123
+            val <!DEPRECATED_COMPOSABLE_PROPERTY!>bar<!>: Int get() = 123
+
+            val foo: Int @Composable get() = 123
         """
         )
     }
@@ -89,9 +94,67 @@
             import androidx.compose.runtime.Composable
 
             @Composable
-            var <!COMPOSABLE_VAR!>bam<!>: Int 
+            var <!DEPRECATED_COMPOSABLE_PROPERTY, COMPOSABLE_VAR!>bam<!>: Int
                 get() { return 123 }
                 set(value) { print(value) }
+
+            var <!COMPOSABLE_VAR!>bam2<!>: Int
+                @Composable get() { return 123 }
+                set(value) { print(value) }
+
+            var <!COMPOSABLE_VAR!>bam3<!>: Int
+                @Composable get() { return 123 }
+                <!WRONG_ANNOTATION_TARGET!>@Composable<!> set(value) { print(value) }
+
+            var <!COMPOSABLE_VAR!>bam4<!>: Int
+                get() { return 123 }
+                <!WRONG_ANNOTATION_TARGET!>@Composable<!> set(value) { print(value) }
+        """
+        )
+    }
+
+    fun testPropertyGetterAllForms() {
+        doTest(
+            """
+            import androidx.compose.runtime.Composable
+
+            @Composable val <!DEPRECATED_COMPOSABLE_PROPERTY!>bar1<!>: Int get() = 123
+            val bar2: Int @Composable get() = 123
+            @get:Composable val bar3: Int get() = 123
+
+            interface Foo {
+                @Composable val <!DEPRECATED_COMPOSABLE_PROPERTY!>bar1<!>: Int get() = 123
+                val bar2: Int @Composable get() = 123
+                @get:Composable val bar3: Int get() = 123
+            }
+        """
+        )
+    }
+
+    fun testMarkedPropInOverrideMarkedGetter() {
+        doTest(
+            """
+            import androidx.compose.runtime.Composable
+            interface A {
+                val foo: Int @Composable get() = 123
+            }
+            class Impl : A {
+                @Composable override val <!DEPRECATED_COMPOSABLE_PROPERTY!>foo<!>: Int get() = 123
+            }
+        """
+        )
+    }
+
+    fun testMarkedGetterInOverrideMarkedProp() {
+        doTest(
+            """
+            import androidx.compose.runtime.Composable
+            interface A {
+                @Composable val <!DEPRECATED_COMPOSABLE_PROPERTY!>foo<!>: Int get() = 123
+            }
+            class Impl : A {
+                override val foo: Int @Composable get() = 123
+            }
         """
         )
     }
@@ -128,16 +191,31 @@
                 @Composable
                 fun composableFunction(param: Boolean): Boolean
                 @Composable
-                val composableProperty: Boolean
+                val <!DEPRECATED_COMPOSABLE_PROPERTY!>composableProperty<!>: Boolean
                 fun nonComposableFunction(param: Boolean): Boolean
                 val nonComposableProperty: Boolean
             }
 
             object FakeFoo : Foo {
                 <!CONFLICTING_OVERLOADS!>override fun composableFunction(param: Boolean)<!> = true
-                <!CONFLICTING_OVERLOADS!>override val composableProperty: Boolean<!> get() = true
+                <!CONFLICTING_OVERLOADS!>override val composableProperty: Boolean<!> <!CONFLICTING_OVERLOADS!>get()<!> = true
                 <!CONFLICTING_OVERLOADS!>@Composable override fun nonComposableFunction(param: Boolean)<!> = true
-                <!CONFLICTING_OVERLOADS!>@Composable override val nonComposableProperty: Boolean<!> get() = true
+                <!CONFLICTING_OVERLOADS!>@Composable override val <!DEPRECATED_COMPOSABLE_PROPERTY!>nonComposableProperty<!>: Boolean<!> <!CONFLICTING_OVERLOADS!>get()<!> = true
+            }
+
+            interface Bar {
+                @Composable
+                fun composableFunction(param: Boolean): Boolean
+                val composableProperty: Boolean @Composable get()
+                fun nonComposableFunction(param: Boolean): Boolean
+                val nonComposableProperty: Boolean
+            }
+
+            object FakeBar : Bar {
+                <!CONFLICTING_OVERLOADS!>override fun composableFunction(param: Boolean)<!> = true
+                <!CONFLICTING_OVERLOADS!>override val composableProperty: Boolean<!> <!CONFLICTING_OVERLOADS!>get()<!> = true
+                <!CONFLICTING_OVERLOADS!>@Composable override fun nonComposableFunction(param: Boolean)<!> = true
+                <!CONFLICTING_OVERLOADS!>override val nonComposableProperty: Boolean<!> <!CONFLICTING_OVERLOADS!>@Composable get()<!> = true
             }
         """
         )
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
index 37784c8..6bd17a8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
@@ -68,6 +68,8 @@
 import org.jetbrains.kotlin.types.upperIfFlexible
 import org.jetbrains.kotlin.util.OperatorNameConventions
 
+internal const val COMPOSABLE_PROPERTIES = true
+
 open class ComposableCallChecker :
     CallChecker,
     AdditionalTypeChecker,
@@ -187,7 +189,11 @@
                 }
                 is KtPropertyAccessor -> {
                     val property = node.property
-                    if (!property.annotationEntries.hasComposableAnnotation(bindingContext)) {
+                    val isComposable = node
+                        .annotationEntries.hasComposableAnnotation(bindingContext)
+                    val propertyIsComposable = property
+                        .annotationEntries.hasComposableAnnotation(bindingContext)
+                    if (!(isComposable || COMPOSABLE_PROPERTIES && propertyIsComposable)) {
                         illegalCall(context, reportOn, property.nameIdentifier ?: property)
                     }
                     return
@@ -329,19 +335,32 @@
     return when (candidateDescriptor) {
         is ValueParameterDescriptor -> false
         is LocalVariableDescriptor -> false
-        is PropertyDescriptor -> candidateDescriptor.hasComposableAnnotation()
-        is PropertyGetterDescriptor ->
-            candidateDescriptor.correspondingProperty.hasComposableAnnotation()
+        is PropertyDescriptor -> {
+            val isGetter = valueArguments.isEmpty()
+            val getter = candidateDescriptor.getter
+            if (isGetter && getter != null) {
+                getter.hasComposableAnnotation() ||
+                    (COMPOSABLE_PROPERTIES && candidateDescriptor.hasComposableAnnotation())
+            } else {
+                false
+            }
+        }
+        is PropertyGetterDescriptor -> candidateDescriptor.hasComposableAnnotation() || (
+            COMPOSABLE_PROPERTIES && candidateDescriptor.correspondingProperty
+                .hasComposableAnnotation()
+            )
         else -> candidateDescriptor.hasComposableAnnotation()
     }
 }
 
 internal fun CallableDescriptor.isMarkedAsComposable(): Boolean {
     return when (this) {
-        is PropertyGetterDescriptor -> correspondingProperty.hasComposableAnnotation()
+        is PropertyGetterDescriptor -> hasComposableAnnotation() || (
+            COMPOSABLE_PROPERTIES && correspondingProperty.hasComposableAnnotation()
+            )
         is ValueParameterDescriptor -> type.hasComposableAnnotation()
         is LocalVariableDescriptor -> type.hasComposableAnnotation()
-        is PropertyDescriptor -> hasComposableAnnotation()
+        is PropertyDescriptor -> COMPOSABLE_PROPERTIES && hasComposableAnnotation()
         else -> hasComposableAnnotation()
     }
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
index 8e592b6..bf9bce7 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
@@ -26,6 +26,7 @@
 import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
 import org.jetbrains.kotlin.descriptors.FunctionDescriptor
 import org.jetbrains.kotlin.descriptors.ModuleDescriptor
+import org.jetbrains.kotlin.descriptors.PropertyAccessorDescriptor
 import org.jetbrains.kotlin.descriptors.PropertyDescriptor
 import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
 import org.jetbrains.kotlin.platform.TargetPlatform
@@ -33,6 +34,7 @@
 import org.jetbrains.kotlin.psi.KtDeclaration
 import org.jetbrains.kotlin.psi.KtFunction
 import org.jetbrains.kotlin.psi.KtProperty
+import org.jetbrains.kotlin.psi.KtPropertyAccessor
 import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker
 import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
 import org.jetbrains.kotlin.types.KotlinType
@@ -53,8 +55,15 @@
         context: DeclarationCheckerContext
     ) {
         when {
-            declaration is KtProperty &&
+            COMPOSABLE_PROPERTIES &&
+                declaration is KtProperty &&
                 descriptor is PropertyDescriptor -> checkProperty(declaration, descriptor, context)
+            declaration is KtPropertyAccessor &&
+                descriptor is PropertyAccessorDescriptor -> checkPropertyAccessor(
+                declaration,
+                descriptor,
+                context
+            )
             declaration is KtFunction &&
                 descriptor is FunctionDescriptor -> checkFunction(declaration, descriptor, context)
         }
@@ -112,9 +121,58 @@
         context: DeclarationCheckerContext
     ) {
         val hasComposableAnnotation = descriptor.hasComposableAnnotation()
+        val thisIsComposable = hasComposableAnnotation || descriptor
+            .getter
+            ?.hasComposableAnnotation() == true
         if (descriptor.overriddenDescriptors.isNotEmpty()) {
             val override = descriptor.overriddenDescriptors.first()
-            if (override.hasComposableAnnotation() != hasComposableAnnotation) {
+            val overrideIsComposable = override.hasComposableAnnotation() ||
+                override.getter?.hasComposableAnnotation() == true
+            if (overrideIsComposable != thisIsComposable) {
+                context.trace.report(
+                    ComposeErrors.CONFLICTING_OVERLOADS.on(
+                        declaration,
+                        listOf(descriptor, override)
+                    )
+                )
+            }
+        }
+        if (hasComposableAnnotation) {
+            context.trace.report(
+                ComposeErrors.DEPRECATED_COMPOSABLE_PROPERTY.on(
+                    declaration.nameIdentifier ?: declaration
+                )
+            )
+        }
+        if (!hasComposableAnnotation) return
+        val initializer = declaration.initializer
+        val name = declaration.nameIdentifier
+        if (initializer != null && name != null) {
+            context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name))
+        }
+        if (descriptor.isVar && name != null) {
+            context.trace.report(COMPOSABLE_VAR.on(name))
+        }
+    }
+
+    private fun checkPropertyAccessor(
+        declaration: KtPropertyAccessor,
+        descriptor: PropertyAccessorDescriptor,
+        context: DeclarationCheckerContext
+    ) {
+        val propertyDescriptor = descriptor.correspondingProperty
+        val propertyPsi = declaration.parent as? KtProperty ?: return
+        val name = propertyPsi.nameIdentifier
+        val initializer = propertyPsi.initializer
+        val hasComposableAnnotation = descriptor.hasComposableAnnotation()
+        val propertyHasComposableAnnotation = COMPOSABLE_PROPERTIES && propertyDescriptor
+            .hasComposableAnnotation()
+        val thisComposable = hasComposableAnnotation || propertyHasComposableAnnotation
+        if (descriptor.overriddenDescriptors.isNotEmpty()) {
+            val override = descriptor.overriddenDescriptors.first()
+            val overrideComposable = override.hasComposableAnnotation() || override
+                .correspondingProperty.hasComposableAnnotation()
+            if (overrideComposable != thisComposable) {
                 context.trace.report(
                     ComposeErrors.CONFLICTING_OVERLOADS.on(
                         declaration,
@@ -124,12 +182,10 @@
             }
         }
         if (!hasComposableAnnotation) return
-        val initializer = declaration.initializer
-        val name = declaration.nameIdentifier
         if (initializer != null && name != null) {
             context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name))
         }
-        if (descriptor.isVar && name != null) {
+        if (propertyDescriptor.isVar && name != null) {
             context.trace.report(COMPOSABLE_VAR.on(name))
         }
     }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
index 2d62be3..e0e887a 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
@@ -90,5 +90,10 @@
             RENDER_TYPE_WITH_ANNOTATIONS,
             RENDER_TYPE_WITH_ANNOTATIONS
         )
+        MAP.put(
+            ComposeErrors.DEPRECATED_COMPOSABLE_PROPERTY,
+            "@Composable properties should be declared with the @Composable annotation " +
+                "on the getter, and not the property itself."
+        )
     }
 }
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
index b416082..8ef2b83 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
@@ -85,6 +85,10 @@
         )
 
     @JvmField
+    var DEPRECATED_COMPOSABLE_PROPERTY: DiagnosticFactory0<PsiElement> =
+        DiagnosticFactory0.create(Severity.WARNING)
+
+    @JvmField
     val ILLEGAL_ASSIGN_TO_UNIONTYPE =
         DiagnosticFactory2.create<KtExpression, Collection<KotlinType>, Collection<KotlinType>>(
             Severity.ERROR
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index bfb6031..1ad22c2 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -37,7 +37,7 @@
 class ComposeIrGenerationExtension(
     @Suppress("unused") private val liveLiteralsEnabled: Boolean = false,
     private val sourceInformationEnabled: Boolean = true,
-    private val intrinsicRememberEnabled: Boolean = false,
+    private val intrinsicRememberEnabled: Boolean = true,
 ) : IrGenerationExtension {
     @OptIn(ObsoleteDescriptorBasedAPI::class)
     override fun generate(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index b55b131..98ae3f3 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -27,7 +27,10 @@
 import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
 import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
 import org.jetbrains.kotlin.config.CompilerConfigurationKey
+import org.jetbrains.kotlin.config.KotlinCompilerVersion
 import org.jetbrains.kotlin.extensions.internal.TypeResolutionInterceptor
 import org.jetbrains.kotlin.serialization.DescriptorSerializer
 
@@ -38,6 +41,8 @@
         CompilerConfigurationKey<Boolean>("Include source information in generated code")
     val INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY =
         CompilerConfigurationKey<Boolean>("Enable optimization to treat remember as an intrinsic")
+    val SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK =
+        CompilerConfigurationKey<Boolean>("Suppress Kotlin version compatibility check")
 }
 
 class ComposeCommandLineProcessor : CommandLineProcessor {
@@ -64,13 +69,21 @@
             required = false,
             allowMultipleOccurrences = false
         )
+        val SUPPRESS_KOTLIN_VERSION_CHECK_ENABLED_OPTION = CliOption(
+            "suppressKotlinVersionCompatibilityCheck",
+            "<true|false>",
+            "Suppress Kotlin version compatibility check",
+            required = false,
+            allowMultipleOccurrences = false
+        )
     }
 
     override val pluginId = PLUGIN_ID
     override val pluginOptions = listOf(
         LIVE_LITERALS_ENABLED_OPTION,
         SOURCE_INFORMATION_ENABLED_OPTION,
-        INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_OPTION
+        INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_OPTION,
+        SUPPRESS_KOTLIN_VERSION_CHECK_ENABLED_OPTION
     )
 
     override fun processOption(
@@ -88,6 +101,10 @@
         )
         INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_OPTION -> configuration.put(
             ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY,
+            value != "false"
+        )
+        SUPPRESS_KOTLIN_VERSION_CHECK_ENABLED_OPTION -> configuration.put(
+            ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK,
             value == "true"
         )
         else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}")
@@ -112,6 +129,26 @@
             project: Project,
             configuration: CompilerConfiguration
         ) {
+            val KOTLIN_VERSION_EXPECTATION = "1.4.20"
+            KotlinCompilerVersion.getVersion()?.let { version ->
+                val suppressKotlinVersionCheck = configuration.get(
+                    ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK,
+                    false
+                )
+                if (!suppressKotlinVersionCheck && version != KOTLIN_VERSION_EXPECTATION) {
+                    val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
+                    msgCollector?.report(
+                        CompilerMessageSeverity.ERROR,
+                        "This version (${VersionChecker.compilerVersion}) of the Compose" +
+                            " Compiler requires Kotlin version $KOTLIN_VERSION_EXPECTATION but" +
+                            " you appear to be using Kotlin version $version which is not known" +
+                            " to be compatible.  Please fix your configuration (or" +
+                            " `suppressKotlinVersionCompatibilityCheck` but don't say I didn't" +
+                            " warn you!)."
+                    )
+                }
+            }
+
             val liveLiteralsEnabled = configuration.get(
                 ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY,
                 false
@@ -122,7 +159,7 @@
             )
             val intrinsicRememberEnabled = configuration.get(
                 ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY,
-                false
+                true
             )
             StorageComponentContainerContributor.registerExtension(
                 project,
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 d5b7b4322..2d8e6da 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
@@ -23,30 +23,33 @@
 
 class VersionChecker(val context: IrPluginContext) {
 
-    /**
-     * A table of version ints to version strings. This should be updated every time
-     * ComposeVersion.kt is updated.
-     */
-    private val versionTable = mapOf(
-        1600 to "0.1.0-dev16",
-        1700 to "1.0.0-alpha06",
-        1800 to "1.0.0-alpha07",
-        1900 to "1.0.0-alpha08"
-    )
+    companion object {
+        /**
+         * A table of version ints to version strings. This should be updated every time
+         * ComposeVersion.kt is updated.
+         */
+        private val versionTable = mapOf(
+            1600 to "0.1.0-dev16",
+            1700 to "1.0.0-alpha06",
+            1800 to "1.0.0-alpha07",
+            1900 to "1.0.0-alpha08",
+            2000 to "1.0.0-alpha09"
+        )
 
-    /**
-     * The minimum version int that this compiler is guaranteed to be compatible with. Typically
-     * this will match the version int that is in ComposeVersion.kt in the runtime.
-     */
-    private val minimumRuntimeVersionInt: Int = 1900
+        /**
+         * The minimum version int that this compiler is guaranteed to be compatible with. Typically
+         * this will match the version int that is in ComposeVersion.kt in the runtime.
+         */
+        private val minimumRuntimeVersionInt: Int = 2000
 
-    /**
-     * The maven version string of this compiler. This string should be updated before/after every
-     * release.
-     */
-    private val compilerVersion: String = "1.0.0-alpha08"
-    private val minimumRuntimeVersion: String
-        get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
+        /**
+         * The maven version string of this compiler. This string should be updated before/after every
+         * release.
+         */
+        val compilerVersion: String = "1.0.0-alpha09"
+        private val minimumRuntimeVersion: String
+            get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
+    }
 
     fun check() {
         val versionClass = context.referenceClass(ComposeFqNames.ComposeVersion)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 1e6f5fe..d852691 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -117,6 +117,7 @@
 import org.jetbrains.kotlin.ir.types.IrSimpleType
 import org.jetbrains.kotlin.ir.types.IrType
 import org.jetbrains.kotlin.ir.types.classOrNull
+import org.jetbrains.kotlin.ir.types.classifierOrFail
 import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl
 import org.jetbrains.kotlin.ir.types.impl.IrStarProjectionImpl
 import org.jetbrains.kotlin.ir.types.isNullable
@@ -720,6 +721,19 @@
         )
     }
 
+    protected fun irGreater(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
+        val int = context.irBuiltIns.intType
+        val gt = context.irBuiltIns.greaterFunByOperandType[int.classifierOrFail]
+        return irCall(
+            gt!!,
+            IrStatementOrigin.GT,
+            null,
+            null,
+            lhs,
+            rhs
+        )
+    }
+
     protected fun irReturn(
         target: IrReturnTargetSymbol,
         value: IrExpression,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index f38d76b..6925da4 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -252,6 +252,7 @@
 interface IrChangedBitMaskValue {
     fun irLowBit(): IrExpression
     fun irIsolateBitsAtSlot(slot: Int, includeStableBit: Boolean): IrExpression
+    fun irSlotAnd(slot: Int, bits: Int): IrExpression
     fun irHasDifferences(): IrExpression
     fun irCopyToTemporary(
         nameHint: String? = null,
@@ -2163,16 +2164,21 @@
 
     private fun encounteredComposableCall(withGroups: Boolean) {
         var scope: Scope? = currentScope
+        // it is important that we only report "withGroups: false" for the _nearest_ scope, and
+        // every scope above that it effectively means there was a group even if it is false
+        var groups = withGroups
         loop@ while (scope != null) {
             when (scope) {
                 is Scope.FunctionScope -> {
-                    scope.recordComposableCall(withGroups)
+                    scope.recordComposableCall(groups)
+                    groups = true
                     if (!scope.isInlinedLambda) {
                         break@loop
                     }
                 }
                 is Scope.BlockScope -> {
-                    scope.recordComposableCall(withGroups)
+                    scope.recordComposableCall(groups)
+                    groups = true
                 }
                 is Scope.ClassScope -> {
                     break@loop
@@ -2684,9 +2690,11 @@
 
         return when {
             meta.isStatic -> irConst(false)
-            meta.isCertain && param is IrChangedBitMaskVariable -> {
-                // if it's a dirty flag then we know that the value is now CERTAIN,
-                // thus we can avoid calling changed all together
+            meta.isCertain &&
+                meta.stability.knownStable() &&
+                param is IrChangedBitMaskVariable -> {
+                // if it's a dirty flag, and the parameter is _guaranteed_ to be stable, then we
+                // know that the value is now CERTAIN, thus we can avoid calling changed completely
                 //
                 // invalid = invalid or (mask == different)
                 irEqual(
@@ -2694,30 +2702,55 @@
                     irConst(ParamState.Different.bitsForSlot(meta.maskSlot))
                 )
             }
-            meta.isCertain && param != null -> {
-                // if it's a changed flag then uncertain is a possible value. If it is uncertain,
-                // then we need to call changed. If it is uncertain here it will _always_ be
-                // uncertain here, so this is safe. If it is not uncertain, we can just check to
-                // see if its different
-                // TODO(lmr): IMPORTANT QUESTION - is unstable + something other than uncertain
-                //  possible?
+            meta.isCertain &&
+                !meta.stability.knownUnstable() &&
+                param is IrChangedBitMaskVariable -> {
+                // if it's a dirty flag, and the parameter might be stable, then we only check
+                // changed if the value is unstable, otherwise we can just check to see if the mask
+                // is different
                 //
-                //
-                //     invalid = invalid or ((mask == uncertain && changed()) || mask == different)
+                // invalid = invalid or (stable && mask == different || unstable && changed)
+
+                val maskIsStableAndDifferent = irEqual(
+                    param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = true),
+                    irConst(ParamState.Different.bitsForSlot(meta.maskSlot))
+                )
+                val stableBits = param.irSlotAnd(meta.maskSlot, StabilityBits.UNSTABLE.bits)
+                val maskIsUnstableAndChanged = irAndAnd(
+                    irNotEqual(stableBits, irConst(0)),
+                    irChanged(arg)
+                )
+                irOrOr(
+                    maskIsStableAndDifferent,
+                    maskIsUnstableAndChanged
+                )
+            }
+            meta.isCertain &&
+                !meta.stability.knownUnstable() &&
+                param != null -> {
+                // if it's a changed flag then uncertain is a possible value. If it is uncertain
+                // OR unstable, then we need to call changed. If it is uncertain or unstable here
+                // it will _always_ be uncertain or unstable here, so this is safe. If it is not
+                // uncertain or unstable, we can just check to see if its different
+
+                //     unstableOrUncertain = mask xor 011 > 010
+                //     invalid = invalid or ((unstableOrUncertain && changed()) || mask == different)
+
+                val maskIsUnstableOrUncertain =
+                    irGreater(
+                        irXor(
+                            param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = true),
+                            irConst(bitsForSlot(0b011, meta.maskSlot))
+                        ),
+                        irConst(bitsForSlot(0b010, meta.maskSlot))
+                    )
                 irOrOr(
                     irAndAnd(
-                        irEqual(
-                            // we do NOT include the stable bit here because we want to capture
-                            // both of the cases where the type is stable and unstable.
-                            param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = false),
-                            // NOTE: this is always "0", but i'm writing it out fully here to
-                            // just make the code more clear
-                            irConst(ParamState.Uncertain.bitsForSlot(meta.maskSlot))
-                        ),
+                        maskIsUnstableOrUncertain,
                         irChanged(arg)
                     ),
                     irEqual(
-                        param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = true),
+                        param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = false),
                         irConst(ParamState.Different.bitsForSlot(meta.maskSlot))
                     )
                 )
@@ -3702,6 +3735,14 @@
             )
         }
 
+        override fun irSlotAnd(slot: Int, bits: Int): IrExpression {
+            // %changed and 0b11
+            return irAnd(
+                irGet(params[paramIndexForSlot(slot)]),
+                irBitsForSlot(bits, slot)
+            )
+        }
+
         override fun irHasDifferences(): IrExpression {
             if (count == 0) {
                 // for 0 slots (no params), we can create a shortcut expression of just checking the
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
index d45ddd1..4bdb3e6 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
@@ -88,6 +88,10 @@
         return super.visitFunction(declaration).also { it.copyMetadataFrom(declaration) }
     }
 
+    override fun visitConstructor(declaration: IrConstructor): IrConstructor {
+        return super.visitConstructor(declaration).also { it.copyMetadataFrom(declaration) }
+    }
+
     override fun visitSimpleFunction(declaration: IrSimpleFunction): IrSimpleFunction {
         return super.visitSimpleFunction(declaration).also {
             it.correspondingPropertySymbol = declaration.correspondingPropertySymbol
@@ -284,6 +288,7 @@
             is IrPropertyImpl -> metadata = owner.metadata
             is IrFunction -> metadata = owner.metadata
             is IrClassImpl -> metadata = owner.metadata
+            else -> throw Error("Unknown type: $this")
         }
     }
 
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
index d9cdd91..e4c97a5 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
@@ -356,7 +356,16 @@
         ).also { fn ->
             newDescriptor.bind(fn)
             if (this is IrSimpleFunction) {
-                fn.correspondingPropertySymbol = correspondingPropertySymbol
+                val propertySymbol = correspondingPropertySymbol
+                if (propertySymbol != null) {
+                    fn.correspondingPropertySymbol = propertySymbol
+                    if (propertySymbol.owner.getter == this) {
+                        propertySymbol.owner.getter = fn
+                    }
+                    if (propertySymbol.owner.setter == this) {
+                        propertySymbol.owner.setter = this
+                    }
+                }
             }
             fn.parent = parent
             fn.typeParameters = this.typeParameters.map {
diff --git a/compose/desktop/desktop/build.gradle b/compose/desktop/desktop/build.gradle
index 5a631af..328931fc 100644
--- a/compose/desktop/desktop/build.gradle
+++ b/compose/desktop/desktop/build.gradle
@@ -21,6 +21,7 @@
 import androidx.build.SupportConfigKt
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
+import static androidx.build.AndroidXPlugin.BUILD_ON_SERVER_TASK
 import static androidx.build.dependencies.DependenciesKt.*
 
 plugins {
@@ -124,6 +125,7 @@
     }
 }
 
-rootProject.tasks.getByName("buildOnServer").configure {
-    dependsOn(":compose:desktop:desktop:jar")
+def projectPath = project.path
+rootProject.tasks.named(BUILD_ON_SERVER_TASK).configure {
+    dependsOn("$projectPath:jvmJar")
 }
\ No newline at end of file
diff --git a/compose/desktop/desktop/samples/build.gradle b/compose/desktop/desktop/samples/build.gradle
index 12c04c9..f81a739 100644
--- a/compose/desktop/desktop/samples/build.gradle
+++ b/compose/desktop/desktop/samples/build.gradle
@@ -18,6 +18,7 @@
 
 import androidx.build.SupportConfigKt
 
+import static androidx.build.AndroidXPlugin.BUILD_ON_SERVER_TASK
 import static androidx.build.dependencies.DependenciesKt.*
 
 plugins {
@@ -91,3 +92,8 @@
 task run {
     dependsOn("run1")
 }
+
+def projectPath = project.path
+rootProject.tasks.named(BUILD_ON_SERVER_TASK).configure {
+    dependsOn("$projectPath:jvmJar")
+}
\ No newline at end of file
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
index 1cb6ffb..fb54ac1 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
@@ -46,7 +46,7 @@
 import androidx.compose.foundation.text.appendInlineContent
 import androidx.compose.material.BottomAppBar
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Checkbox
 import androidx.compose.material.CircularProgressIndicator
 import androidx.compose.material.ExtendedFloatingActionButton
@@ -68,11 +68,10 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphicsLayer
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
-import androidx.compose.ui.input.key.ExperimentalKeyInput
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.plus
 import androidx.compose.ui.input.key.shortcuts
@@ -83,7 +82,7 @@
 import androidx.compose.ui.text.PlaceholderVerticalAlign
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.fontFamily
 import androidx.compose.ui.text.platform.font
 import androidx.compose.ui.text.style.TextAlign
@@ -133,7 +132,7 @@
                     IconButton(
                         onClick = {}
                     ) {
-                        Icon(Icons.Filled.Menu, Modifier.size(ButtonConstants.DefaultIconSize))
+                        Icon(Icons.Filled.Menu, Modifier.size(ButtonDefaults.IconSize))
                     }
                 }
             },
@@ -158,7 +157,6 @@
     )
 }
 
-@OptIn(ExperimentalKeyInput::class)
 @Composable
 private fun ScrollableContent(scrollState: ScrollState) {
     val amount = remember { mutableStateOf(0) }
@@ -179,7 +177,7 @@
         val inlineIndicatorId = "indicator"
 
         Text(
-            text = annotatedString {
+            text = buildAnnotatedString {
                 append("The quick ")
                 if (animation.value) {
                     appendInlineContent(inlineIndicatorId)
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
index a36969c..2dc8318 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
@@ -18,6 +18,7 @@
 import androidx.compose.desktop.AppManager
 import androidx.compose.desktop.AppWindow
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
@@ -31,9 +32,11 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.AlertDialog
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
 import androidx.compose.material.Text
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Checkbox
 import androidx.compose.material.RadioButton
 import androidx.compose.material.Surface
@@ -156,7 +159,8 @@
                         .background(color = Color(255, 255, 255, 10))
                         .fillMaxWidth()
                 ) {
-                    Spacer(modifier = Modifier.height(60.dp))
+                    ContextMenu()
+                    Spacer(modifier = Modifier.height(30.dp))
                     Spacer(modifier = Modifier.height(60.dp))
                     Row {
                         Checkbox(
@@ -313,7 +317,7 @@
     val buttonHover = remember { mutableStateOf(false) }
     Button(
         onClick = onClick,
-        colors = ButtonConstants.defaultButtonColors(
+        colors = ButtonDefaults.buttonColors(
             backgroundColor =
                 if (buttonHover.value)
                     Color(color.red / 1.3f, color.green / 1.3f, color.blue / 1.3f)
@@ -339,9 +343,9 @@
 }
 
 @Composable
-fun TextBox(text: String = "") {
+fun TextBox(text: String = "", modifier: Modifier = Modifier.height(30.dp)) {
     Box(
-        modifier = Modifier.height(30.dp),
+        modifier = modifier,
         contentAlignment = Alignment.Center
     ) {
         Text(
@@ -352,6 +356,45 @@
 }
 
 @Composable
+fun ContextMenu() {
+    val items = listOf("Item A", "Item B", "Item C", "Item D", "Item E", "Item F")
+    val showMenu = remember { mutableStateOf(false) }
+    val selectedIndex = remember { mutableStateOf(0) }
+
+    Surface(
+        modifier = Modifier
+            .padding(start = 4.dp, top = 2.dp),
+        color = Color(255, 255, 255, 40),
+        shape = RoundedCornerShape(4.dp)
+    ) {
+        DropdownMenu(
+            toggle = {
+                TextBox(
+                    text = "Selected: ${items[selectedIndex.value]}",
+                    modifier = Modifier
+                        .height(26.dp)
+                        .padding(start = 4.dp, end = 4.dp)
+                        .clickable(onClick = { showMenu.value = true })
+                )
+            },
+            expanded = showMenu.value,
+            onDismissRequest = { showMenu.value = false }
+        ) {
+            items.forEachIndexed { index, name ->
+                DropdownMenuItem(
+                    onClick = {
+                        selectedIndex.value = index
+                        showMenu.value = false
+                    }
+                ) {
+                    Text(text = name)
+                }
+            }
+        }
+    }
+}
+
+@Composable
 fun RadioButton(text: String, state: MutableState<Boolean>) {
     Box(
         modifier = Modifier.height(30.dp),
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
index c8141df..bfb6073 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
@@ -18,36 +18,35 @@
 import androidx.compose.desktop.AppManager
 import androidx.compose.desktop.AppWindowAmbient
 import androidx.compose.desktop.ComposePanel
-import androidx.compose.desktop.setContent
 import androidx.compose.desktop.Window
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.Text
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Button
 import androidx.compose.material.Surface
+import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
 import java.awt.BorderLayout
 import java.awt.Dimension
 import java.awt.event.ActionEvent
 import java.awt.event.ActionListener
-import javax.swing.JFrame
 import javax.swing.JButton
+import javax.swing.JFrame
 import javax.swing.WindowConstants
 
 val northClicks = mutableStateOf(0)
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index 9d96717..afd1086 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -90,6 +90,10 @@
     property public default float spacing;
   }
 
+  public final class AspectRatioKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier aspectRatio(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float ratio, optional boolean matchHeightConstraintsFirst);
+  }
+
   public final class BoxKt {
     method @androidx.compose.runtime.Composable public static inline void Box(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Box(androidx.compose.ui.Modifier modifier);
@@ -340,54 +344,9 @@
     enum_constant public static final androidx.compose.foundation.layout.IntrinsicSize Min;
   }
 
-  public final class LayoutAspectRatioKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier aspectRatio(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float ratio, optional boolean matchHeightConstraintsFirst);
-  }
-
-  public final class LayoutOffsetKt {
-    method public static androidx.compose.ui.Modifier absoluteOffset(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> x, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> y);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absoluteOffset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
-    method @Deprecated public static androidx.compose.ui.Modifier absoluteOffsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
-    method public static androidx.compose.ui.Modifier offset(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> x, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> y);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier offset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
-    method @Deprecated public static androidx.compose.ui.Modifier offsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
-  }
-
-  public final class LayoutPaddingKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absolutePadding-w2-DAAU(androidx.compose.ui.Modifier, optional float left, optional float top, optional float right, optional float bottom);
-    method public static androidx.compose.ui.Modifier padding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues padding);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-S2lCeAQ(androidx.compose.ui.Modifier, optional float horizontal, optional float vertical);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-w2-DAAU(androidx.compose.ui.Modifier, optional float start, optional float top, optional float end, optional float bottom);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-wxomhCo(androidx.compose.ui.Modifier, float all);
-  }
-
   @kotlin.DslMarker public @interface LayoutScopeMarker {
   }
 
-  public final class LayoutSizeKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier defaultMinSizeConstraints-S2lCeAQ(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height-wxomhCo(androidx.compose.ui.Modifier, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier heightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeight-wxomhCo(androidx.compose.ui.Modifier, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-wxomhCo(androidx.compose.ui.Modifier, float size);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidth-wxomhCo(androidx.compose.ui.Modifier, float width);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-wxomhCo(androidx.compose.ui.Modifier, float size);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier sizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier width-wxomhCo(androidx.compose.ui.Modifier, float width);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier widthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentHeight(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Vertical align, optional boolean unbounded);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentSize(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment align, optional boolean unbounded);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentWidth(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Horizontal align, optional boolean unbounded);
-  }
-
   public enum MainAxisAlignment {
     enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Center;
     enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment End;
@@ -397,6 +356,23 @@
     enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Start;
   }
 
+  public final class OffsetKt {
+    method public static androidx.compose.ui.Modifier absoluteOffset(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.unit.IntOffset> offset);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absoluteOffset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
+    method @Deprecated public static androidx.compose.ui.Modifier absoluteOffsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
+    method public static androidx.compose.ui.Modifier offset(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.unit.IntOffset> offset);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier offset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
+    method @Deprecated public static androidx.compose.ui.Modifier offsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
+  }
+
+  public final class PaddingKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absolutePadding-w2-DAAU(androidx.compose.ui.Modifier, optional float left, optional float top, optional float right, optional float bottom);
+    method public static androidx.compose.ui.Modifier padding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues padding);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-S2lCeAQ(androidx.compose.ui.Modifier, optional float horizontal, optional float vertical);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-w2-DAAU(androidx.compose.ui.Modifier, optional float start, optional float top, optional float end, optional float bottom);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-wxomhCo(androidx.compose.ui.Modifier, float all);
+  }
+
   @androidx.compose.runtime.Immutable public final class PaddingValues {
     method public float component1-D9Ej5fM();
     method public float component2-D9Ej5fM();
@@ -434,6 +410,30 @@
   public static final class RowScope.Companion implements androidx.compose.foundation.layout.RowScope {
   }
 
+  public final class SizeKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier defaultMinSizeConstraints-S2lCeAQ(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height-wxomhCo(androidx.compose.ui.Modifier, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier heightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeight-wxomhCo(androidx.compose.ui.Modifier, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-wxomhCo(androidx.compose.ui.Modifier, float size);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidth-wxomhCo(androidx.compose.ui.Modifier, float width);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-wxomhCo(androidx.compose.ui.Modifier, float size);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier sizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier width-wxomhCo(androidx.compose.ui.Modifier, float width);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier widthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentHeight(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Vertical align, optional boolean unbounded);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentSize(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment align, optional boolean unbounded);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentWidth(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Horizontal align, optional boolean unbounded);
+  }
+
   public enum SizeMode {
     enum_constant public static final androidx.compose.foundation.layout.SizeMode Expand;
     enum_constant public static final androidx.compose.foundation.layout.SizeMode Wrap;
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 9d96717..afd1086 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -90,6 +90,10 @@
     property public default float spacing;
   }
 
+  public final class AspectRatioKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier aspectRatio(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float ratio, optional boolean matchHeightConstraintsFirst);
+  }
+
   public final class BoxKt {
     method @androidx.compose.runtime.Composable public static inline void Box(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Box(androidx.compose.ui.Modifier modifier);
@@ -340,54 +344,9 @@
     enum_constant public static final androidx.compose.foundation.layout.IntrinsicSize Min;
   }
 
-  public final class LayoutAspectRatioKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier aspectRatio(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float ratio, optional boolean matchHeightConstraintsFirst);
-  }
-
-  public final class LayoutOffsetKt {
-    method public static androidx.compose.ui.Modifier absoluteOffset(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> x, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> y);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absoluteOffset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
-    method @Deprecated public static androidx.compose.ui.Modifier absoluteOffsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
-    method public static androidx.compose.ui.Modifier offset(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> x, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> y);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier offset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
-    method @Deprecated public static androidx.compose.ui.Modifier offsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
-  }
-
-  public final class LayoutPaddingKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absolutePadding-w2-DAAU(androidx.compose.ui.Modifier, optional float left, optional float top, optional float right, optional float bottom);
-    method public static androidx.compose.ui.Modifier padding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues padding);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-S2lCeAQ(androidx.compose.ui.Modifier, optional float horizontal, optional float vertical);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-w2-DAAU(androidx.compose.ui.Modifier, optional float start, optional float top, optional float end, optional float bottom);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-wxomhCo(androidx.compose.ui.Modifier, float all);
-  }
-
   @kotlin.DslMarker public @interface LayoutScopeMarker {
   }
 
-  public final class LayoutSizeKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier defaultMinSizeConstraints-S2lCeAQ(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height-wxomhCo(androidx.compose.ui.Modifier, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier heightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeight-wxomhCo(androidx.compose.ui.Modifier, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-wxomhCo(androidx.compose.ui.Modifier, float size);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidth-wxomhCo(androidx.compose.ui.Modifier, float width);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-wxomhCo(androidx.compose.ui.Modifier, float size);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier sizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier width-wxomhCo(androidx.compose.ui.Modifier, float width);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier widthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentHeight(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Vertical align, optional boolean unbounded);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentSize(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment align, optional boolean unbounded);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentWidth(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Horizontal align, optional boolean unbounded);
-  }
-
   public enum MainAxisAlignment {
     enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Center;
     enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment End;
@@ -397,6 +356,23 @@
     enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Start;
   }
 
+  public final class OffsetKt {
+    method public static androidx.compose.ui.Modifier absoluteOffset(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.unit.IntOffset> offset);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absoluteOffset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
+    method @Deprecated public static androidx.compose.ui.Modifier absoluteOffsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
+    method public static androidx.compose.ui.Modifier offset(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.unit.IntOffset> offset);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier offset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
+    method @Deprecated public static androidx.compose.ui.Modifier offsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
+  }
+
+  public final class PaddingKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absolutePadding-w2-DAAU(androidx.compose.ui.Modifier, optional float left, optional float top, optional float right, optional float bottom);
+    method public static androidx.compose.ui.Modifier padding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues padding);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-S2lCeAQ(androidx.compose.ui.Modifier, optional float horizontal, optional float vertical);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-w2-DAAU(androidx.compose.ui.Modifier, optional float start, optional float top, optional float end, optional float bottom);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-wxomhCo(androidx.compose.ui.Modifier, float all);
+  }
+
   @androidx.compose.runtime.Immutable public final class PaddingValues {
     method public float component1-D9Ej5fM();
     method public float component2-D9Ej5fM();
@@ -434,6 +410,30 @@
   public static final class RowScope.Companion implements androidx.compose.foundation.layout.RowScope {
   }
 
+  public final class SizeKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier defaultMinSizeConstraints-S2lCeAQ(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height-wxomhCo(androidx.compose.ui.Modifier, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier heightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeight-wxomhCo(androidx.compose.ui.Modifier, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredHeightIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSize-wxomhCo(androidx.compose.ui.Modifier, float size);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredSizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidth-wxomhCo(androidx.compose.ui.Modifier, float width);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier preferredWidthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-S2lCeAQ(androidx.compose.ui.Modifier, float width, float height);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier size-wxomhCo(androidx.compose.ui.Modifier, float size);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier sizeIn-w2-DAAU(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight, optional float maxWidth, optional float maxHeight);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier width-wxomhCo(androidx.compose.ui.Modifier, float width);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier widthIn-S2lCeAQ(androidx.compose.ui.Modifier, optional float min, optional float max);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentHeight(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Vertical align, optional boolean unbounded);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentSize(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment align, optional boolean unbounded);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentWidth(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Horizontal align, optional boolean unbounded);
+  }
+
   public enum SizeMode {
     enum_constant public static final androidx.compose.foundation.layout.SizeMode Expand;
     enum_constant public static final androidx.compose.foundation.layout.SizeMode Wrap;
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 540542a..94376e8 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -90,10 +90,14 @@
     property public default float spacing;
   }
 
+  public final class AspectRatioKt {
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier aspectRatio(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float ratio, optional boolean matchHeightConstraintsFirst);
+  }
+
   public final class BoxKt {
     method @androidx.compose.runtime.Composable public static inline void Box(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Box(androidx.compose.ui.Modifier modifier);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.node.LayoutNode.MeasureBlocks rememberMeasureBlocks(androidx.compose.ui.Alignment alignment);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.node.MeasureBlocks rememberMeasureBlocks(androidx.compose.ui.Alignment alignment);
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface BoxScope {
@@ -121,8 +125,8 @@
 
   public final class ColumnKt {
     method @androidx.compose.runtime.Composable public static inline void Column(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.node.LayoutNode.MeasureBlocks columnMeasureBlocks(androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, androidx.compose.ui.Alignment.Horizontal horizontalAlignment);
-    field @kotlin.PublishedApi internal static final androidx.compose.ui.node.LayoutNode.MeasureBlocks DefaultColumnMeasureBlocks;
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.node.MeasureBlocks columnMeasureBlocks(androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, androidx.compose.ui.Alignment.Horizontal horizontalAlignment);
+    field @kotlin.PublishedApi internal static final androidx.compose.ui.node.MeasureBlocks DefaultColumnMeasureBlocks;
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface ColumnScope {
@@ -343,20 +347,28 @@
     enum_constant public static final androidx.compose.foundation.layout.IntrinsicSize Min;
   }
 
-  public final class LayoutAspectRatioKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier aspectRatio(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float ratio, optional boolean matchHeightConstraintsFirst);
+  @kotlin.DslMarker public @interface LayoutScopeMarker {
   }
 
-  public final class LayoutOffsetKt {
-    method public static androidx.compose.ui.Modifier absoluteOffset(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> x, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> y);
+  public enum MainAxisAlignment {
+    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Center;
+    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment End;
+    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment SpaceAround;
+    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment SpaceBetween;
+    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment SpaceEvenly;
+    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Start;
+  }
+
+  public final class OffsetKt {
+    method public static androidx.compose.ui.Modifier absoluteOffset(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.unit.IntOffset> offset);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absoluteOffset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
     method @Deprecated public static androidx.compose.ui.Modifier absoluteOffsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
-    method public static androidx.compose.ui.Modifier offset(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> x, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,java.lang.Float> y);
+    method public static androidx.compose.ui.Modifier offset(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Density,androidx.compose.ui.unit.IntOffset> offset);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier offset-S2lCeAQ(androidx.compose.ui.Modifier, optional float x, optional float y);
     method @Deprecated public static androidx.compose.ui.Modifier offsetPx(androidx.compose.ui.Modifier, optional androidx.compose.runtime.State<java.lang.Float> x, optional androidx.compose.runtime.State<java.lang.Float> y);
   }
 
-  public final class LayoutPaddingKt {
+  public final class PaddingKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier absolutePadding-w2-DAAU(androidx.compose.ui.Modifier, optional float left, optional float top, optional float right, optional float bottom);
     method public static androidx.compose.ui.Modifier padding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues padding);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-S2lCeAQ(androidx.compose.ui.Modifier, optional float horizontal, optional float vertical);
@@ -364,10 +376,47 @@
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier padding-wxomhCo(androidx.compose.ui.Modifier, float all);
   }
 
-  @kotlin.DslMarker public @interface LayoutScopeMarker {
+  @androidx.compose.runtime.Immutable public final class PaddingValues {
+    method public float component1-D9Ej5fM();
+    method public float component2-D9Ej5fM();
+    method public float component3-D9Ej5fM();
+    method public float component4-D9Ej5fM();
+    method @androidx.compose.runtime.Immutable public androidx.compose.foundation.layout.PaddingValues copy-ZmiikuI(float start, float top, float end, float bottom);
+    method public float getBottom-D9Ej5fM();
+    method public float getEnd-D9Ej5fM();
+    method public float getStart-D9Ej5fM();
+    method public float getTop-D9Ej5fM();
+    property public final float bottom;
+    property public final float end;
+    property public final float start;
+    property public final float top;
   }
 
-  public final class LayoutSizeKt {
+  public final class RowColumnImplKt {
+    method @kotlin.PublishedApi internal static androidx.compose.ui.node.MeasureBlocks rowColumnMeasureBlocks-GZ6WFlY(androidx.compose.foundation.layout.LayoutOrientation orientation, kotlin.jvm.functions.Function5<? super java.lang.Integer,? super int[],? super androidx.compose.ui.unit.LayoutDirection,? super androidx.compose.ui.unit.Density,? super int[],kotlin.Unit> arrangement, float arrangementSpacing, androidx.compose.foundation.layout.SizeMode crossAxisSize, androidx.compose.foundation.layout.CrossAxisAlignment crossAxisAlignment);
+  }
+
+  public final class RowKt {
+    method @androidx.compose.runtime.Composable public static inline void Row(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.node.MeasureBlocks rowMeasureBlocks(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, androidx.compose.ui.Alignment.Vertical verticalAlignment);
+    field @kotlin.PublishedApi internal static final androidx.compose.ui.node.MeasureBlocks DefaultRowMeasureBlocks;
+  }
+
+  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface RowScope {
+    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier align(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Vertical alignment);
+    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, androidx.compose.ui.layout.HorizontalAlignmentLine alignmentLine);
+    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
+    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignByBaseline(androidx.compose.ui.Modifier);
+    method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.HorizontalAlignmentLine alignmentLine);
+    method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
+    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
+    field public static final androidx.compose.foundation.layout.RowScope.Companion Companion;
+  }
+
+  public static final class RowScope.Companion implements androidx.compose.foundation.layout.RowScope {
+  }
+
+  public final class SizeKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier defaultMinSizeConstraints-S2lCeAQ(androidx.compose.ui.Modifier, optional float minWidth, optional float minHeight);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
@@ -391,55 +440,6 @@
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier wrapContentWidth(androidx.compose.ui.Modifier, optional androidx.compose.ui.Alignment.Horizontal align, optional boolean unbounded);
   }
 
-  public enum MainAxisAlignment {
-    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Center;
-    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment End;
-    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment SpaceAround;
-    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment SpaceBetween;
-    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment SpaceEvenly;
-    enum_constant public static final androidx.compose.foundation.layout.MainAxisAlignment Start;
-  }
-
-  @androidx.compose.runtime.Immutable public final class PaddingValues {
-    method public float component1-D9Ej5fM();
-    method public float component2-D9Ej5fM();
-    method public float component3-D9Ej5fM();
-    method public float component4-D9Ej5fM();
-    method @androidx.compose.runtime.Immutable public androidx.compose.foundation.layout.PaddingValues copy-ZmiikuI(float start, float top, float end, float bottom);
-    method public float getBottom-D9Ej5fM();
-    method public float getEnd-D9Ej5fM();
-    method public float getStart-D9Ej5fM();
-    method public float getTop-D9Ej5fM();
-    property public final float bottom;
-    property public final float end;
-    property public final float start;
-    property public final float top;
-  }
-
-  public final class RowColumnImplKt {
-    method @kotlin.PublishedApi internal static androidx.compose.ui.node.LayoutNode.MeasureBlocks rowColumnMeasureBlocks-GZ6WFlY(androidx.compose.foundation.layout.LayoutOrientation orientation, kotlin.jvm.functions.Function5<? super java.lang.Integer,? super int[],? super androidx.compose.ui.unit.LayoutDirection,? super androidx.compose.ui.unit.Density,? super int[],kotlin.Unit> arrangement, float arrangementSpacing, androidx.compose.foundation.layout.SizeMode crossAxisSize, androidx.compose.foundation.layout.CrossAxisAlignment crossAxisAlignment);
-  }
-
-  public final class RowKt {
-    method @androidx.compose.runtime.Composable public static inline void Row(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.node.LayoutNode.MeasureBlocks rowMeasureBlocks(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, androidx.compose.ui.Alignment.Vertical verticalAlignment);
-    field @kotlin.PublishedApi internal static final androidx.compose.ui.node.LayoutNode.MeasureBlocks DefaultRowMeasureBlocks;
-  }
-
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface RowScope {
-    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier align(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Vertical alignment);
-    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, androidx.compose.ui.layout.HorizontalAlignmentLine alignmentLine);
-    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier alignByBaseline(androidx.compose.ui.Modifier);
-    method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, androidx.compose.ui.layout.HorizontalAlignmentLine alignmentLine);
-    method @Deprecated public default androidx.compose.ui.Modifier alignWithSiblings(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
-    method @androidx.compose.runtime.Stable public default androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, @FloatRange(from=0.0, to=3.4E38, fromInclusive=false) float weight, optional boolean fill);
-    field public static final androidx.compose.foundation.layout.RowScope.Companion Companion;
-  }
-
-  public static final class RowScope.Companion implements androidx.compose.foundation.layout.RowScope {
-  }
-
   public enum SizeMode {
     enum_constant public static final androidx.compose.foundation.layout.SizeMode Expand;
     enum_constant public static final androidx.compose.foundation.layout.SizeMode Wrap;
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 0d9b4e1..af73d79 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -118,7 +118,6 @@
 
 tasks.withType(KotlinCompile).configureEach {
     kotlinOptions {
-        freeCompilerArgs += ["-XXLanguage:-NewInference"]
         useIR = true
     }
 }
diff --git a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/LayoutOffsetSample.kt b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/LayoutOffsetSample.kt
index dd01667..03262c02 100644
--- a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/LayoutOffsetSample.kt
+++ b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/LayoutOffsetSample.kt
@@ -23,11 +23,14 @@
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.gesture.tapGestureFilter
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 
 @Sampled
@@ -60,12 +63,12 @@
 fun OffsetPxModifier() {
     // This text will be offset in steps of 10.dp from the top left of the available space in
     // left-to-right context, and from top right in right-to-left context.
-    val offset = remember { mutableStateOf(0f) }
+    var offset by remember { mutableStateOf(0) }
     Text(
         "Layout offset modifier sample",
         Modifier
-            .tapGestureFilter { offset.value += 10f }
-            .offset({ offset.value }, { offset.value })
+            .tapGestureFilter { offset += 10 }
+            .offset { IntOffset(offset, offset) }
     )
 }
 
@@ -73,11 +76,11 @@
 @Composable
 fun AbsoluteOffsetPxModifier() {
     // This text will be offset in steps of 10.dp from the top left of the available space.
-    val offset = remember { mutableStateOf(0f) }
+    var offset by remember { mutableStateOf(0) }
     Text(
         "Layout offset modifier sample",
         Modifier
-            .tapGestureFilter { offset.value += 10f }
-            .absoluteOffset({ offset.value }, { offset.value })
+            .tapGestureFilter { offset += 10 }
+            .absoluteOffset { IntOffset(offset, offset) }
     )
 }
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAspectRatioTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AspectRatioTest.kt
similarity index 99%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAspectRatioTest.kt
rename to compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AspectRatioTest.kt
index eafd7e9..0ecfd38 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAspectRatioTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AspectRatioTest.kt
@@ -41,7 +41,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class LayoutAspectRatioTest : LayoutTest() {
+class AspectRatioTest : LayoutTest() {
     @Before
     fun before() {
         isDebugInspectorInfoEnabled = true
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt
index d235e93..c64b175 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/BoxTest.kt
@@ -18,9 +18,11 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInParent
@@ -453,6 +455,46 @@
     }
 
     @Test
+    fun testBox_childAffectsBoxSize() {
+        var layoutLatch = CountDownLatch(2)
+        val size = mutableStateOf(10.dp)
+        var measure = 0
+        var layout = 0
+        show {
+            Box {
+                Layout(
+                    content = {
+                        Box {
+                            Box(
+                                Modifier.size(size.value, 10.dp).onGloballyPositioned {
+                                    layoutLatch.countDown()
+                                }
+                            )
+                        }
+                    }
+                ) { measurables, constraints ->
+                    val placeable = measurables.first().measure(constraints)
+                    ++measure
+                    layout(placeable.width, placeable.height) {
+                        placeable.place(0, 0)
+                        ++layout
+                        layoutLatch.countDown()
+                    }
+                }
+            }
+        }
+        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
+        assertEquals(1, measure)
+        assertEquals(1, layout)
+
+        layoutLatch = CountDownLatch(2)
+        activityTestRule.runOnUiThread { size.value = 20.dp }
+        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
+        assertEquals(2, measure)
+        assertEquals(2, layout)
+    }
+
+    @Test
     fun testBox_hasCorrectIntrinsicMeasurements() = with(density) {
         val testWidth = 90.toDp()
         val testHeight = 80.toDp()
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ContainerTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ContainerTest.kt
deleted file mode 100644
index c2b1893..0000000
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ContainerTest.kt
+++ /dev/null
@@ -1,513 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.layout
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.node.Ref
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.SmallTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import kotlin.math.roundToInt
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class ContainerTest : LayoutTest() {
-    @Test
-    fun testContainer_wrapsChild() = with(density) {
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(1)
-        val containerSize = Ref<IntSize>()
-        show {
-            Box {
-                Container(
-                    Modifier.onGloballyPositioned { coordinates ->
-                        containerSize.value = coordinates.size
-                        positionedLatch.countDown()
-                    }
-                ) {
-                    EmptyBox(width = sizeDp, height = sizeDp)
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        assertEquals(IntSize(size, size), containerSize.value)
-    }
-
-    @Test
-    fun testContainer_appliesPaddingToChild() = with(density) {
-        val paddingDp = 20.dp
-        val padding = paddingDp.toIntPx()
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(2)
-        val containerSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Box {
-                Container(
-                    padding = PaddingValues(paddingDp),
-                    modifier = Modifier.onGloballyPositioned { coordinates ->
-                        containerSize.value = coordinates.size
-                        positionedLatch.countDown()
-                    }
-                ) {
-                    EmptyBox(
-                        width = sizeDp, height = sizeDp,
-                        modifier = Modifier.onGloballyPositioned { coordinates ->
-                            childPosition.value = coordinates.positionInRoot
-                            positionedLatch.countDown()
-                        }
-                    )
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        val totalPadding = paddingDp.toIntPx() * 2
-        assertEquals(
-            IntSize(size + totalPadding, size + totalPadding),
-            containerSize.value
-        )
-        assertEquals(Offset(padding.toFloat(), padding.toFloat()), childPosition.value)
-    }
-
-    @Test
-    fun testContainer_passesConstraintsToChild() = with(density) {
-        val sizeDp = 100.dp
-        val childWidthDp = 20.dp
-        val childWidth = childWidthDp.toIntPx()
-        val childHeightDp = 30.dp
-        val childHeight = childHeightDp.toIntPx()
-        val childConstraints = DpConstraints.fixed(childWidthDp, childHeightDp)
-
-        val positionedLatch = CountDownLatch(4)
-        val containerSize = Ref<IntSize>()
-        val childSize = Array(3) { IntSize(0, 0) }
-        show {
-            Box {
-                Row(
-                    Modifier.onGloballyPositioned { coordinates ->
-                        containerSize.value = coordinates.size
-                        positionedLatch.countDown()
-                    }
-                ) {
-                    Container(width = childWidthDp, height = childHeightDp) {
-                        EmptyBox(
-                            width = sizeDp, height = sizeDp,
-                            modifier = Modifier.onGloballyPositioned { coordinates ->
-                                childSize[0] = coordinates.size
-                                positionedLatch.countDown()
-                            }
-                        )
-                    }
-                    Container(constraints = childConstraints) {
-                        EmptyBox(
-                            width = sizeDp, height = sizeDp,
-                            modifier = Modifier.onGloballyPositioned { coordinates ->
-                                childSize[1] = coordinates.size
-                                positionedLatch.countDown()
-                            }
-                        )
-                    }
-                    Container(
-                        constraints = (childConstraints),
-                        // These should have priority.
-                        width = (childWidthDp * 2),
-                        height = (childHeightDp * 2)
-                    ) {
-                        EmptyBox(
-                            width = sizeDp, height = sizeDp,
-                            modifier = Modifier.onGloballyPositioned { coordinates ->
-                                childSize[2] = coordinates.size
-                                positionedLatch.countDown()
-                            }
-                        )
-                    }
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        assertEquals(IntSize(childWidth, childHeight), childSize[0])
-        assertEquals(IntSize(childWidth, childHeight), childSize[1])
-        assertEquals(
-            IntSize((childWidthDp * 2).toIntPx(), (childHeightDp * 2).toIntPx()),
-            childSize[2]
-        )
-    }
-
-    @Test
-    fun testContainer_fillsAvailableSpace_whenSizeIsMax() = with(density) {
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(3)
-        val alignSize = Ref<IntSize>()
-        val containerSize = Ref<IntSize>()
-        val childSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Container(
-                alignment = Alignment.TopStart,
-                modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
-                    alignSize.value = coordinates.size
-                    positionedLatch.countDown()
-                }
-            ) {
-                Container(
-                    expanded = true,
-                    modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        containerSize.value = coordinates.size
-                        positionedLatch.countDown()
-                    }
-                ) {
-                    EmptyBox(
-                        width = sizeDp,
-                        height = sizeDp,
-                        modifier = Modifier.onGloballyPositioned { coordinates ->
-                            childSize.value = coordinates.size
-                            childPosition.value = coordinates.positionInRoot
-                            positionedLatch.countDown()
-                        }
-                    )
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        assertEquals(alignSize.value, containerSize.value)
-        assertEquals(IntSize(size, size), childSize.value)
-        assertEquals(
-            Offset(
-                (containerSize.value!!.width.toFloat() / 2 - size.toFloat() / 2)
-                    .roundToInt().toFloat(),
-                (containerSize.value!!.height.toFloat() / 2 - size.toFloat() / 2)
-                    .roundToInt().toFloat()
-            ),
-            childPosition.value
-        )
-    }
-
-    @Test
-    fun testContainer_respectsIncomingMinConstraints() = with(density) {
-        // Start with an even number of Int to avoid rounding issues due to different DPI
-        // I.e, if we fix Dp instead, it's possible that when we convert to Px, sizeDp can round
-        // down but sizeDp * 2 can round up, causing a 1 pixel test error.
-        val size = 200
-        val sizeDp = size.toDp()
-
-        val positionedLatch = CountDownLatch(2)
-        val containerSize = Ref<IntSize>()
-        val childSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Box {
-                val constraints = DpConstraints(minWidth = sizeDp * 2, minHeight = sizeDp * 2)
-                ConstrainedBox(
-                    modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        containerSize.value = coordinates.size
-                        positionedLatch.countDown()
-                    },
-                    constraints = constraints
-                ) {
-                    Container(alignment = Alignment.BottomEnd) {
-                        EmptyBox(
-                            width = sizeDp, height = sizeDp,
-                            modifier = Modifier.onGloballyPositioned { coordinates ->
-                                childSize.value = coordinates.size
-                                childPosition.value =
-                                    coordinates.positionInRoot
-                                positionedLatch.countDown()
-                            }
-                        )
-                    }
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        assertEquals(
-            IntSize((sizeDp * 2).toIntPx(), (sizeDp * 2).toIntPx()),
-            containerSize.value
-        )
-        assertEquals(IntSize(size, size), childSize.value)
-        assertEquals(Offset(size.toFloat(), size.toFloat()), childPosition.value)
-    }
-
-    @Test
-    fun testContainer_hasTheRightSize_withPaddingAndNoChildren() = with(density) {
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-
-        val containerSize = Ref<IntSize>()
-        val latch = CountDownLatch(1)
-        show {
-            Box {
-                Container(
-                    width = sizeDp, height = sizeDp, padding = PaddingValues(10.dp),
-                    modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        containerSize.value = coordinates.size
-                        latch.countDown()
-                    }
-                ) {
-                }
-            }
-        }
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-
-        assertEquals(IntSize(size, size), containerSize.value)
-    }
-
-    @Test
-    fun testContainer_correctlyAppliesNonSymmetricPadding() = with(density) {
-        val childSizeDp = 50.toDp()
-        val paddingLeft = 8.toDp()
-        val paddingTop = 7.toDp()
-        val paddingRight = 5.toDp()
-        val paddingBottom = 10.toDp()
-        val innerPadding = PaddingValues(
-            start = paddingLeft,
-            top = paddingTop,
-            end = paddingRight,
-            bottom = paddingBottom
-        )
-        val expectedSize = IntSize(
-            childSizeDp.toIntPx() + paddingLeft.toIntPx() + paddingRight.toIntPx(),
-            childSizeDp.toIntPx() + paddingTop.toIntPx() + paddingBottom.toIntPx()
-        )
-
-        var containerSize: IntSize? = null
-        val latch = CountDownLatch(1)
-        show {
-            Box {
-                Container(
-                    Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        containerSize = coordinates.size
-                        latch.countDown()
-                    },
-                    padding = innerPadding
-                ) {
-                    Spacer(Modifier.preferredSize(width = childSizeDp, height = childSizeDp))
-                }
-            }
-        }
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-
-        assertEquals(expectedSize, containerSize)
-    }
-
-    @Test
-    fun testContainer_contentSmallerThanPaddingIsCentered() = with(density) {
-        val containerSize = 50.toDp()
-        val padding = 10.toDp()
-        val childSize = 5.toDp()
-        val innerPadding = PaddingValues(padding)
-
-        var childCoordinates: LayoutCoordinates? = null
-        val latch = CountDownLatch(1)
-        show {
-            Box {
-                Container(width = containerSize, height = containerSize, padding = innerPadding) {
-                    Spacer(
-                        Modifier
-                            .preferredSize(width = childSize, height = childSize)
-                            .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                                childCoordinates = coordinates
-                                latch.countDown()
-                            }
-                    )
-                }
-            }
-        }
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-
-        val centeringOffset = padding.toIntPx() +
-            (
-                (
-                    containerSize.toIntPx() - padding.toIntPx() * 2 -
-                        childSize.toIntPx()
-                    ) / 2f
-                ).roundToInt()
-        val childPosition = childCoordinates!!.parentCoordinates!!.childToLocal(
-            childCoordinates!!,
-            Offset.Zero
-        )
-        assertEquals(
-            Offset(centeringOffset.toFloat(), centeringOffset.toFloat()),
-            childPosition
-        )
-        assertEquals(IntSize(childSize.toIntPx(), childSize.toIntPx()), childCoordinates!!.size)
-    }
-
-    @Test
-    fun testContainer_childAffectsContainerSize() {
-        var layoutLatch = CountDownLatch(2)
-        val size = mutableStateOf(10.dp)
-        var measure = 0
-        var layout = 0
-        show {
-            Box {
-                Layout(
-                    content = {
-                        Container {
-                            EmptyBox(
-                                width = size.value,
-                                height = 10.dp,
-                                modifier = Modifier.onGloballyPositioned {
-                                    layoutLatch.countDown()
-                                }
-                            )
-                        }
-                    }
-                ) { measurables, constraints ->
-                    val placeable = measurables.first().measure(constraints)
-                    ++measure
-                    layout(placeable.width, placeable.height) {
-                        placeable.place(0, 0)
-                        ++layout
-                        layoutLatch.countDown()
-                    }
-                }
-            }
-        }
-        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
-        assertEquals(1, measure)
-        assertEquals(1, layout)
-
-        layoutLatch = CountDownLatch(2)
-        activityTestRule.runOnUiThread { size.value = 20.dp }
-        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
-        assertEquals(2, measure)
-        assertEquals(2, layout)
-    }
-
-    @Test
-    fun testContainer_childDoesNotAffectContainerSize_whenSizeIsMax() {
-        var layoutLatch = CountDownLatch(2)
-        val size = mutableStateOf(10.dp)
-        var measure = 0
-        var layout = 0
-        show {
-            Box {
-                Layout(
-                    content = {
-                        Container(expanded = true) {
-                            EmptyBox(
-                                width = size.value,
-                                height = 10.dp,
-                                modifier = Modifier.onGloballyPositioned {
-                                    layoutLatch.countDown()
-                                }
-                            )
-                        }
-                    }
-                ) { measurables, constraints ->
-                    val placeable = measurables.first().measure(constraints)
-                    ++measure
-                    layout(placeable.width, placeable.height) {
-                        placeable.place(0, 0)
-                        ++layout
-                        layoutLatch.countDown()
-                    }
-                }
-            }
-        }
-        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
-        assertEquals(1, measure)
-        assertEquals(1, layout)
-
-        layoutLatch = CountDownLatch(1)
-        activityTestRule.runOnUiThread { size.value = 20.dp }
-        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
-        assertEquals(1, measure)
-        assertEquals(1, layout)
-    }
-
-    @Test
-    fun testContainer_childDoesNotAffectContainerSize_whenFixedWidthAndHeight() {
-        var layoutLatch = CountDownLatch(2)
-        val size = mutableStateOf(10.dp)
-        var measure = 0
-        var layout = 0
-        show {
-            Box {
-                Layout(
-                    content = {
-                        Container(width = 20.dp, height = 20.dp) {
-                            EmptyBox(
-                                width = size.value,
-                                height = 10.dp,
-                                modifier = Modifier.onGloballyPositioned {
-                                    layoutLatch.countDown()
-                                }
-                            )
-                        }
-                    }
-                ) { measurables, constraints ->
-                    val placeable = measurables.first().measure(constraints)
-                    ++measure
-                    layout(placeable.width, placeable.height) {
-                        placeable.placeRelative(0, 0)
-                        ++layout
-                        layoutLatch.countDown()
-                    }
-                }
-            }
-        }
-        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
-        assertEquals(1, measure)
-        assertEquals(1, layout)
-
-        layoutLatch = CountDownLatch(1)
-        activityTestRule.runOnUiThread { size.value = 20.dp }
-        assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
-        assertEquals(1, measure)
-        assertEquals(1, layout)
-    }
-
-    @Composable
-    fun EmptyBox(width: Dp, height: Dp, modifier: Modifier = Modifier) {
-        Layout(modifier = modifier, content = { }) { _, constraints ->
-            layout(
-                constraints.constrainWidth(width.toIntPx()),
-                constraints.constrainHeight(height.toIntPx())
-            ) {}
-        }
-    }
-}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt
deleted file mode 100644
index 7a9fe53..0000000
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt
+++ /dev/null
@@ -1,487 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.layout
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Providers
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.positionInParent
-import androidx.compose.ui.node.Ref
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AmbientLayoutDirection
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.IntSize.Companion.Zero
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.enforce
-import androidx.test.filters.SmallTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import kotlin.math.roundToInt
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class LayoutAlignTest : LayoutTest() {
-    @Test
-    fun test2DWrapContentSize() = with(density) {
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(2)
-        val alignSize = Ref<IntSize>()
-        val alignPosition = Ref<Offset>()
-        val childSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Container(Modifier.saveLayoutInfo(alignSize, alignPosition, positionedLatch)) {
-                Container(
-                    Modifier.fillMaxSize()
-                        .wrapContentSize(Alignment.BottomEnd)
-                        .preferredSize(sizeDp)
-                        .saveLayoutInfo(childSize, childPosition, positionedLatch)
-                ) {
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        val root = findOwnerView()
-        waitForDraw(root)
-
-        assertEquals(IntSize(root.width, root.height), alignSize.value)
-        assertEquals(Offset(0f, 0f), alignPosition.value)
-        assertEquals(IntSize(size, size), childSize.value)
-        assertEquals(
-            Offset(root.width - size.toFloat(), root.height - size.toFloat()),
-            childPosition.value
-        )
-    }
-
-    @Test
-    fun test1DWrapContentSize() = with(density) {
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(2)
-        val alignSize = Ref<IntSize>()
-        val alignPosition = Ref<Offset>()
-        val childSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Container(
-                Modifier.saveLayoutInfo(
-                    size = alignSize,
-                    position = alignPosition,
-                    positionedLatch = positionedLatch
-                )
-            ) {
-                Container(
-                    Modifier.fillMaxSize()
-                        .wrapContentWidth(Alignment.End)
-                        .preferredWidth(sizeDp)
-                        .saveLayoutInfo(childSize, childPosition, positionedLatch)
-                ) {
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        val root = findOwnerView()
-        waitForDraw(root)
-
-        assertEquals(IntSize(root.width, root.height), alignSize.value)
-        assertEquals(Offset(0f, 0f), alignPosition.value)
-        assertEquals(IntSize(size, root.height), childSize.value)
-        assertEquals(Offset(root.width - size.toFloat(), 0f), childPosition.value)
-    }
-
-    @Test
-    fun testWrapContentSize_rtl() = with(density) {
-        val sizeDp = 200.toDp()
-        val size = sizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(3)
-        val childSize = Array(3) { Ref<IntSize>() }
-        val childPosition = Array(3) { Ref<Offset>() }
-        show {
-            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-                Box(Modifier.fillMaxSize()) {
-                    Box(Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
-                        Box(
-                            Modifier.preferredSize(sizeDp)
-                                .saveLayoutInfo(childSize[0], childPosition[0], positionedLatch)
-                        ) {
-                        }
-                    }
-                    Box(Modifier.fillMaxSize().wrapContentHeight(Alignment.CenterVertically)) {
-                        Box(
-                            Modifier.preferredSize(sizeDp)
-                                .saveLayoutInfo(childSize[1], childPosition[1], positionedLatch)
-                        ) {
-                        }
-                    }
-                    Box(Modifier.fillMaxSize().wrapContentSize(Alignment.BottomEnd)) {
-                        Box(
-                            Modifier.preferredSize(sizeDp)
-                                .saveLayoutInfo(childSize[2], childPosition[2], positionedLatch)
-                        ) {
-                        }
-                    }
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        val root = findOwnerView()
-        waitForDraw(root)
-
-        assertEquals(
-            Offset((root.width - size).toFloat(), 0f),
-            childPosition[0].value
-        )
-        assertEquals(
-            Offset(
-                (root.width - size).toFloat(),
-                ((root.height - size) / 2).toFloat()
-            ),
-            childPosition[1].value
-        )
-        assertEquals(
-            Offset(0f, (root.height - size).toFloat()),
-            childPosition[2].value
-        )
-    }
-
-    @Test
-    fun testModifier_wrapsContent() = with(density) {
-        val contentSize = 50.dp
-        val size = Ref<IntSize>()
-        val latch = CountDownLatch(1)
-        show {
-            Container {
-                Container(Modifier.saveLayoutInfo(size, Ref(), latch)) {
-                    Container(
-                        Modifier.wrapContentSize(Alignment.TopStart)
-                            .preferredSize(contentSize)
-                    ) {}
-                }
-            }
-        }
-
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-        assertEquals(IntSize(contentSize.toIntPx(), contentSize.toIntPx()), size.value)
-    }
-
-    @Test
-    fun testWrapContentSize_wrapsContent_whenMeasuredWithInfiniteConstraints() = with(density) {
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(2)
-        val alignSize = Ref<IntSize>()
-        val alignPosition = Ref<Offset>()
-        val childSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Layout(
-                content = {
-                    Container(
-                        Modifier.saveLayoutInfo(alignSize, alignPosition, positionedLatch)
-                    ) {
-                        Container(
-                            Modifier.wrapContentSize(Alignment.BottomEnd)
-                                .preferredSize(sizeDp)
-                                .saveLayoutInfo(childSize, childPosition, positionedLatch)
-                        ) {
-                        }
-                    }
-                },
-                measureBlock = { measurables, constraints ->
-                    val placeable = measurables.first().measure(Constraints())
-                    layout(constraints.maxWidth, constraints.maxHeight) {
-                        placeable.placeRelative(0, 0)
-                    }
-                }
-            )
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        val root = findOwnerView()
-        waitForDraw(root)
-
-        assertEquals(IntSize(size, size), alignSize.value)
-        assertEquals(Offset(0f, 0f), alignPosition.value)
-        assertEquals(IntSize(size, size), childSize.value)
-        assertEquals(Offset(0f, 0f), childPosition.value)
-    }
-
-    @Test
-    fun testWrapContentSize_respectsMinConstraints() = with(density) {
-        val sizeDp = 50.dp
-        val size = sizeDp.toIntPx()
-        val doubleSizeDp = sizeDp * 2
-        val doubleSize = doubleSizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(2)
-        val wrapSize = Ref<IntSize>()
-        val childSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Container(Modifier.wrapContentSize(Alignment.TopStart)) {
-                Layout(
-                    modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        wrapSize.value = coordinates.size
-                        positionedLatch.countDown()
-                    },
-                    content = {
-                        Container(
-                            Modifier.wrapContentSize(Alignment.Center)
-                                .preferredSize(sizeDp)
-                                .saveLayoutInfo(childSize, childPosition, positionedLatch)
-                        ) {
-                        }
-                    },
-                    measureBlock = { measurables, incomingConstraints ->
-                        val measurable = measurables.first()
-                        val constraints = Constraints(
-                            minWidth = doubleSizeDp.toIntPx(),
-                            minHeight = doubleSizeDp.toIntPx()
-                        ).enforce(incomingConstraints)
-                        val placeable = measurable.measure(constraints)
-                        layout(placeable.width, placeable.height) {
-                            placeable.placeRelative(IntOffset.Zero)
-                        }
-                    }
-                )
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        assertEquals(IntSize(doubleSize, doubleSize), wrapSize.value)
-        assertEquals(IntSize(size, size), childSize.value)
-        assertEquals(
-            Offset(
-                ((doubleSize - size) / 2f).roundToInt().toFloat(),
-                ((doubleSize - size) / 2f).roundToInt().toFloat()
-            ),
-            childPosition.value
-        )
-    }
-
-    @Test
-    fun testWrapContentSize_unbounded() = with(density) {
-        val outerSize = 10f
-        val innerSize = 20f
-
-        val positionedLatch = CountDownLatch(4)
-        show {
-            Box(
-                Modifier.size(outerSize.toDp())
-                    .onGloballyPositioned {
-                        assertEquals(outerSize, it.size.width.toFloat())
-                        positionedLatch.countDown()
-                    }
-            ) {
-                Box(
-                    Modifier.wrapContentSize(Alignment.BottomEnd, unbounded = true)
-                        .size(innerSize.toDp())
-                        .onGloballyPositioned {
-                            assertEquals(
-                                Offset(outerSize - innerSize, outerSize - innerSize),
-                                it.positionInParent
-                            )
-                            positionedLatch.countDown()
-                        }
-                )
-                Box(
-                    Modifier.wrapContentWidth(Alignment.End, unbounded = true)
-                        .size(innerSize.toDp())
-                        .onGloballyPositioned {
-                            assertEquals(outerSize - innerSize, it.positionInParent.x)
-                            positionedLatch.countDown()
-                        }
-                )
-                Box(
-                    Modifier.wrapContentHeight(Alignment.Bottom, unbounded = true)
-                        .size(innerSize.toDp())
-                        .onGloballyPositioned {
-                            assertEquals(outerSize - innerSize, it.positionInParent.y)
-                            positionedLatch.countDown()
-                        }
-                )
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-    }
-
-    // TODO(popam): this should be unit test instead
-    @Test
-    fun testAlignmentCoordinates_evenSize() {
-        val size = IntSize(2, 2)
-        assertEquals(IntOffset(0, 0), Alignment.TopStart.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(1, 0), Alignment.TopCenter.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(2, 0), Alignment.TopEnd.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(0, 1), Alignment.CenterStart.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(1, 1), Alignment.Center.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(2, 1), Alignment.CenterEnd.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(0, 2), Alignment.BottomStart.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(1, 2), Alignment.BottomCenter.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(2, 2), Alignment.BottomEnd.align(Zero, size, LayoutDirection.Ltr))
-    }
-
-    // TODO(popam): this should be unit test instead
-    @Test
-    fun testAlignmentCoordinates_oddSize() {
-        val size = IntSize(3, 3)
-        assertEquals(IntOffset(0, 0), Alignment.TopStart.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(2, 0), Alignment.TopCenter.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(3, 0), Alignment.TopEnd.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(0, 2), Alignment.CenterStart.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(2, 2), Alignment.Center.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(3, 2), Alignment.CenterEnd.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(0, 3), Alignment.BottomStart.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(2, 3), Alignment.BottomCenter.align(Zero, size, LayoutDirection.Ltr))
-        assertEquals(IntOffset(3, 3), Alignment.BottomEnd.align(Zero, size, LayoutDirection.Ltr))
-    }
-
-    @Test
-    fun test2DAlignedModifier_hasCorrectIntrinsicMeasurements() = with(density) {
-        testIntrinsics(
-            @Composable {
-                Container(Modifier.wrapContentSize(Alignment.TopStart).aspectRatio(2f)) { }
-            }
-        ) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight ->
-            // Min width.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals(25.dp.toIntPx() * 2, minIntrinsicWidth(25.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), minIntrinsicWidth(Constraints.Infinity))
-
-            // Min height.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), minIntrinsicHeight(50.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), minIntrinsicHeight(Constraints.Infinity))
-
-            // Max width.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals(25.dp.toIntPx() * 2, maxIntrinsicWidth(25.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), maxIntrinsicWidth(Constraints.Infinity))
-
-            // Max height.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), maxIntrinsicHeight(50.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), maxIntrinsicHeight(Constraints.Infinity))
-        }
-    }
-
-    @Test
-    fun test1DAlignedModifier_hasCorrectIntrinsicMeasurements() = with(density) {
-        testIntrinsics({
-            Container(
-                Modifier.wrapContentHeight(Alignment.CenterVertically)
-                    .aspectRatio(2f)
-            ) { }
-        }) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight ->
-
-            // Min width.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals(25.dp.toIntPx() * 2, minIntrinsicWidth(25.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), minIntrinsicWidth(Constraints.Infinity))
-
-            // Min height.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), minIntrinsicHeight(50.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), minIntrinsicHeight(Constraints.Infinity))
-
-            // Max width.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals(25.dp.toIntPx() * 2, maxIntrinsicWidth(25.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), maxIntrinsicWidth(Constraints.Infinity))
-
-            // Max height.
-            assertEquals(0, minIntrinsicWidth(0))
-            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), maxIntrinsicHeight(50.dp.toIntPx()))
-            assertEquals(0.dp.toIntPx(), maxIntrinsicHeight(Constraints.Infinity))
-        }
-    }
-
-    @Test
-    fun testAlignedModifier_alignsCorrectly_whenOddDimensions_endAligned() = with(density) {
-        // Given a 100 x 100 pixel container, we want to make sure that when aligning a 1 x 1 pixel
-        // child to both ends (bottom, and right) we correctly position children at the last
-        // possible pixel, and avoid rounding issues. Previously we first centered the coordinates,
-        // and then aligned after, so the maths would actually be (99 / 2) * 2, which incorrectly
-        // ends up at 100 (Int rounds up) - so the last pixels in both directions just wouldn't
-        // be visible.
-        val parentSize = 100.toDp()
-        val childSizeDp = 1.toDp()
-        val childSizeIpx = childSizeDp.toIntPx()
-
-        val positionedLatch = CountDownLatch(2)
-        val alignSize = Ref<IntSize>()
-        val alignPosition = Ref<Offset>()
-        val childSize = Ref<IntSize>()
-        val childPosition = Ref<Offset>()
-        show {
-            Layout(
-                content = {
-                    Container(
-                        Modifier.preferredSize(parentSize)
-                            .saveLayoutInfo(alignSize, alignPosition, positionedLatch)
-                    ) {
-                        Container(
-                            Modifier.fillMaxSize()
-                                .wrapContentSize(Alignment.BottomEnd)
-                                .preferredSize(childSizeDp)
-                                .saveLayoutInfo(childSize, childPosition, positionedLatch)
-                        ) {
-                        }
-                    }
-                },
-                measureBlock = { measurables, constraints ->
-                    val placeable = measurables.first().measure(Constraints())
-                    layout(constraints.maxWidth, constraints.maxHeight) {
-                        placeable.placeRelative(0, 0)
-                    }
-                }
-            )
-        }
-        positionedLatch.await(1, TimeUnit.SECONDS)
-
-        val root = findOwnerView()
-        waitForDraw(root)
-
-        assertEquals(IntSize(childSizeIpx, childSizeIpx), childSize.value)
-        assertEquals(
-            Offset(
-                (alignSize.value!!.width - childSizeIpx).toFloat(),
-                (alignSize.value!!.height - childSizeIpx).toFloat()
-            ),
-            childPosition.value
-        )
-    }
-}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutDirectionModifierTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutDirectionModifierTest.kt
deleted file mode 100644
index c0fa4a1..0000000
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutDirectionModifierTest.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.layout
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Providers
-import androidx.compose.runtime.emptyContent
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.Ref
-import androidx.compose.ui.platform.AmbientLayoutDirection
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.test.filters.SmallTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class LayoutDirectionModifierTest : LayoutTest() {
-
-    @Test
-    fun testModifiedLayoutDirection_inMeasureScope() {
-        val latch = CountDownLatch(1)
-        val resultLayoutDirection = Ref<LayoutDirection>()
-
-        show {
-            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-                Layout(content = @Composable {}) { _, _ ->
-                    resultLayoutDirection.value = layoutDirection
-                    latch.countDown()
-                    layout(0, 0) {}
-                }
-            }
-        }
-
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-        assertTrue(LayoutDirection.Rtl == resultLayoutDirection.value)
-    }
-
-    @Test
-    fun testModifiedLayoutDirection_inIntrinsicsMeasure() {
-        val latch = CountDownLatch(1)
-        var resultLayoutDirection: LayoutDirection? = null
-
-        show {
-            @OptIn(ExperimentalLayout::class)
-            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-                Layout(
-                    content = @Composable {},
-                    modifier = Modifier.preferredWidth(IntrinsicSize.Max),
-                    minIntrinsicWidthMeasureBlock = { _, _ -> 0 },
-                    minIntrinsicHeightMeasureBlock = { _, _ -> 0 },
-                    maxIntrinsicWidthMeasureBlock = { _, _ ->
-                        resultLayoutDirection = this.layoutDirection
-                        latch.countDown()
-                        0
-                    },
-                    maxIntrinsicHeightMeasureBlock = { _, _ -> 0 }
-                ) { _, _ ->
-                    layout(0, 0) {}
-                }
-            }
-        }
-
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-        assertNotNull(resultLayoutDirection)
-        assertTrue(LayoutDirection.Rtl == resultLayoutDirection)
-    }
-
-    @Test
-    fun testRestoreLocaleLayoutDirection() {
-        val latch = CountDownLatch(1)
-        val resultLayoutDirection = Ref<LayoutDirection>()
-
-        show {
-            val initialLayoutDirection = AmbientLayoutDirection.current
-            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-                Box {
-                    Providers(AmbientLayoutDirection provides initialLayoutDirection) {
-                        Layout(emptyContent()) { _, _ ->
-                            resultLayoutDirection.value = layoutDirection
-                            latch.countDown()
-                            layout(0, 0) {}
-                        }
-                    }
-                }
-            }
-        }
-
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-        assertEquals(LayoutDirection.Ltr, resultLayoutDirection.value)
-    }
-}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
index 2fdccc7..722150b 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
@@ -36,7 +36,7 @@
 import androidx.compose.ui.node.Ref
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -95,22 +95,22 @@
         activityTestRule.runOnUiThread(runnable)
     }
 
-    internal fun findOwnerView(): View {
-        return findOwner(activity).view
+    internal fun findComposeView(): View {
+        return findViewRootForTest(activity).view
     }
 
-    internal fun findOwner(activity: Activity): AndroidOwner {
+    internal fun findViewRootForTest(activity: Activity): ViewRootForTest {
         val contentViewGroup = activity.findViewById<ViewGroup>(android.R.id.content)
-        return findOwner(contentViewGroup)!!
+        return findViewRootForTest(contentViewGroup)!!
     }
 
-    internal fun findOwner(parent: ViewGroup): AndroidOwner? {
+    internal fun findViewRootForTest(parent: ViewGroup): ViewRootForTest? {
         for (index in 0 until parent.childCount) {
             val child = parent.getChildAt(index)
-            if (child is AndroidOwner) {
+            if (child is ViewRootForTest) {
                 return child
             } else if (child is ViewGroup) {
-                val owner = findOwner(child)
+                val owner = findViewRootForTest(child)
                 if (owner != null) {
                     return owner
                 }
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
similarity index 90%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
rename to compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
index fd818bd..4d70733 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -50,7 +51,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-class LayoutOffsetTest : LayoutTest() {
+class OffsetTest : LayoutTest() {
     @get:Rule
     val rule = createComposeRule()
 
@@ -198,7 +199,7 @@
             Box(
                 Modifier.testTag("box")
                     .wrapContentSize(Alignment.TopStart)
-                    .offset({ offsetX }, { offsetY })
+                    .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                     .onGloballyPositioned { coordinates: LayoutCoordinates ->
                         positionX = coordinates.positionInRoot.x
                         positionY = coordinates.positionInRoot.y
@@ -217,11 +218,11 @@
     @Test
     fun offsetPx_positionIsModified_rtl() = with(density) {
         val containerWidth = 30.dp
-        val boxSize = 1f
-        val offsetX = 10f
-        val offsetY = 20f
-        var positionX = 0f
-        var positionY = 0f
+        val boxSize = 1
+        val offsetX = 10
+        val offsetY = 20
+        var positionX = 0
+        var positionY = 0
         rule.setContent {
             Providers((AmbientLayoutDirection provides LayoutDirection.Rtl)) {
                 Box(
@@ -229,10 +230,10 @@
                         .wrapContentSize(Alignment.TopEnd)
                         .preferredWidth(containerWidth)
                         .wrapContentSize(Alignment.TopStart)
-                        .offset({ offsetX }, { offsetY })
+                        .offset { IntOffset(offsetX, offsetY) }
                         .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                            positionX = coordinates.positionInRoot.x
-                            positionY = coordinates.positionInRoot.y
+                            positionX = coordinates.positionInRoot.x.roundToInt()
+                            positionY = coordinates.positionInRoot.y.roundToInt()
                         }
                 ) {
                     // TODO(soboleva): this box should not be needed after b/154758475 is fixed.
@@ -244,7 +245,7 @@
         rule.onNodeWithTag("box").assertExists()
         rule.runOnIdle {
             Assert.assertEquals(
-                containerWidth.toIntPx() - offsetX.roundToInt() - boxSize,
+                containerWidth.toIntPx() - offsetX - boxSize,
                 positionX
             )
             Assert.assertEquals(offsetY, positionY)
@@ -253,18 +254,18 @@
 
     @Test
     fun absoluteOffsetPx_positionIsModified() = with(density) {
-        val offsetX = 10f
-        val offsetY = 20f
-        var positionX = 0f
-        var positionY = 0f
+        val offsetX = 10
+        val offsetY = 20
+        var positionX = 0
+        var positionY = 0
         rule.setContent {
             Box(
                 Modifier.testTag("box")
                     .wrapContentSize(Alignment.TopStart)
-                    .absoluteOffset({ offsetX }, { offsetY })
+                    .absoluteOffset { IntOffset(offsetX, offsetY) }
                     .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        positionX = coordinates.positionInRoot.x
-                        positionY = coordinates.positionInRoot.y
+                        positionX = coordinates.positionInRoot.x.roundToInt()
+                        positionY = coordinates.positionInRoot.y.roundToInt()
                     }
             ) {
             }
@@ -280,11 +281,11 @@
     @Test
     fun absoluteOffsetPx_positionIsModified_rtl() = with(density) {
         val containerWidth = 30.dp
-        val boxSize = 1f
-        val offsetX = 10f
-        val offsetY = 20f
-        var positionX = 0f
-        var positionY = 0f
+        val boxSize = 1
+        val offsetX = 10
+        val offsetY = 20
+        var positionX = 0
+        var positionY = 0
         rule.setContent {
             Providers((AmbientLayoutDirection provides LayoutDirection.Rtl)) {
                 Box(
@@ -292,10 +293,10 @@
                         .wrapContentSize(Alignment.TopEnd)
                         .preferredWidth(containerWidth)
                         .wrapContentSize(Alignment.TopStart)
-                        .absoluteOffset({ offsetX }, { offsetY })
+                        .absoluteOffset { IntOffset(offsetX, offsetY) }
                         .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                            positionX = coordinates.positionInRoot.x
-                            positionY = coordinates.positionInRoot.y
+                            positionX = coordinates.positionInRoot.x.roundToInt()
+                            positionY = coordinates.positionInRoot.y.roundToInt()
                         }
                 ) {
                     // TODO(soboleva): this box should not be needed after b/154758475 is fixed.
@@ -307,7 +308,7 @@
         rule.onNodeWithTag("box").assertExists()
         rule.runOnIdle {
             Assert.assertEquals(
-                containerWidth.toIntPx() - boxSize + offsetX.roundToInt(),
+                containerWidth.toIntPx() - boxSize + offsetX,
                 positionX
             )
             Assert.assertEquals(offsetY, positionY)
@@ -338,20 +339,20 @@
 
     @Test
     fun testOffsetPxInspectableValue() {
-        val modifier = Modifier.offset({ 10.0f }, { 20.0f }) as InspectableValue
+        val modifier = Modifier.offset { IntOffset(10, 20) } as InspectableValue
         assertThat(modifier.nameFallback).isEqualTo("offset")
         assertThat(modifier.valueOverride).isNull()
         assertThat(modifier.inspectableElements.map { it.name }.asIterable())
-            .containsExactly("x", "y")
+            .containsExactly("offset")
     }
 
     @Test
     fun testAbsoluteOffsetPxInspectableValue() {
-        val modifier = Modifier.absoluteOffset({ 10.0f }, { 20.0f }) as InspectableValue
+        val modifier = Modifier.absoluteOffset { IntOffset(10, 20) } as InspectableValue
         assertThat(modifier.nameFallback).isEqualTo("absoluteOffset")
         assertThat(modifier.valueOverride).isNull()
         assertThat(modifier.inspectableElements.map { it.name }.asIterable())
-            .containsExactly("x", "y")
+            .containsExactly("offset")
     }
 
     @Test
@@ -362,7 +363,7 @@
             Box(
                 Modifier
                     .size(10.dp)
-                    .offset(x = { offset })
+                    .offset { IntOffset(offset.roundToInt(), 0) }
                     .drawBehind {
                         contentRedrawsCount ++
                     }
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OnGloballyPositionedTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OnGloballyPositionedTest.kt
deleted file mode 100644
index 22da4ae..0000000
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OnGloballyPositionedTest.kt
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.compose.foundation.layout
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.emptyContent
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.VerticalAlignmentLine
-import androidx.compose.ui.layout.positionInParent
-import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Assert
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import kotlin.math.min
-import kotlin.math.roundToInt
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class OnGloballyPositionedTest : LayoutTest() {
-
-    @Test
-    fun simplePadding() = with(density) {
-        val paddingLeftPx = 100.0f
-        val paddingTopPx = 120.0f
-        var realLeft: Float? = null
-        var realTop: Float? = null
-
-        val positionedLatch = CountDownLatch(1)
-        show {
-            Container(
-                Modifier.fillMaxSize()
-                    .padding(start = paddingLeftPx.toDp(), top = paddingTopPx.toDp())
-                    .onGloballyPositioned {
-                        realLeft = it.positionInParent.x
-                        realTop = it.positionInParent.y
-                        positionedLatch.countDown()
-                    }
-            ) {
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        assertThat(paddingLeftPx).isEqualTo(realLeft)
-        assertThat(paddingTopPx).isEqualTo(realTop)
-    }
-
-    @Test
-    fun nestedLayoutCoordinates() = with(density) {
-        val firstPaddingPx = 10f
-        val secondPaddingPx = 20f
-        val thirdPaddingPx = 30f
-        var gpCoordinates: LayoutCoordinates? = null
-        var childCoordinates: LayoutCoordinates? = null
-
-        val positionedLatch = CountDownLatch(2)
-        show {
-            Container(
-                Modifier.padding(start = firstPaddingPx.toDp()).then(
-                    Modifier.onGloballyPositioned {
-                        gpCoordinates = it
-                        positionedLatch.countDown()
-                    }
-                )
-            ) {
-                Container(Modifier.padding(start = secondPaddingPx.toDp())) {
-                    Container(
-                        Modifier.fillMaxSize()
-                            .padding(start = thirdPaddingPx.toDp())
-                            .onGloballyPositioned {
-                                childCoordinates = it
-                                positionedLatch.countDown()
-                            }
-                    ) {
-                    }
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        // global position
-        val gPos = childCoordinates!!.localToGlobal(Offset.Zero).x
-        assertThat(gPos).isEqualTo((firstPaddingPx + secondPaddingPx + thirdPaddingPx))
-        // Position in grandparent Px(value=50.0)
-        val gpPos = gpCoordinates!!.childToLocal(childCoordinates!!, Offset.Zero).x
-        assertThat(gpPos).isEqualTo((secondPaddingPx + thirdPaddingPx))
-        // local position
-        assertThat(childCoordinates!!.positionInParent.x).isEqualTo(thirdPaddingPx)
-    }
-
-    @Test
-    fun globalCoordinatesAreInActivityCoordinates() = with(density) {
-        val padding = 30
-        val localPosition = Offset.Zero
-        val framePadding = Offset(padding.toFloat(), padding.toFloat())
-        var realGlobalPosition: Offset? = null
-        var realLocalPosition: Offset? = null
-        var frameGlobalPosition: Offset? = null
-
-        val positionedLatch = CountDownLatch(1)
-        activityTestRule.runOnUiThread(object : Runnable {
-            override fun run() {
-                val frameLayout = ComposeView(activity)
-                frameLayout.setPadding(padding, padding, padding, padding)
-                activity.setContentView(frameLayout)
-
-                val position = IntArray(2)
-                frameLayout.getLocationOnScreen(position)
-                frameGlobalPosition = Offset(position[0].toFloat(), position[1].toFloat())
-
-                frameLayout.setContent {
-                    Container(
-                        Modifier.onGloballyPositioned {
-                            realGlobalPosition = it.localToGlobal(localPosition)
-                            realLocalPosition = it.globalToLocal(
-                                framePadding +
-                                    frameGlobalPosition!!
-                            )
-                            positionedLatch.countDown()
-                        },
-                        expanded = true,
-                        content = emptyContent()
-                    )
-                }
-            }
-        })
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        assertThat(realGlobalPosition).isEqualTo(frameGlobalPosition!! + framePadding)
-        assertThat(realLocalPosition).isEqualTo(localPosition)
-    }
-
-    @Test
-    fun justAddedOnPositionedCallbackFiredWithoutLayoutChanges() = with(density) {
-        val needCallback = mutableStateOf(false)
-
-        val positionedLatch = CountDownLatch(1)
-        show {
-            val modifier = if (needCallback.value) {
-                Modifier.onGloballyPositioned { positionedLatch.countDown() }
-            } else {
-                Modifier
-            }
-            Container(modifier, expanded = true) { }
-        }
-
-        activityTestRule.runOnUiThread { needCallback.value = true }
-
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-    }
-
-    @Test
-    fun testRepositionTriggersCallback() {
-        val left = mutableStateOf(30.dp)
-        var realLeft: Float? = null
-
-        var positionedLatch = CountDownLatch(1)
-        show {
-            Box {
-                Container(
-                    Modifier.onGloballyPositioned {
-                        realLeft = it.positionInParent.x
-                        positionedLatch.countDown()
-                    }
-                        .fillMaxSize()
-                        .padding(start = left.value),
-                    content = emptyContent()
-                )
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        positionedLatch = CountDownLatch(1)
-        activityTestRule.runOnUiThread { left.value = 40.dp }
-
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-        with(density) {
-            assertThat(realLeft).isEqualTo(40.dp.toPx())
-        }
-    }
-
-    @Test
-    fun testGrandParentRepositionTriggersChildrenCallback() {
-        // when we reposition any parent layout is causes the change in global
-        // position of all the children down the tree(for example during the scrolling).
-        // children should be able to react on this change.
-        val left = mutableStateOf(20.dp)
-        var realLeft: Float? = null
-        var positionedLatch = CountDownLatch(1)
-        show {
-            Box {
-                Offset(left) {
-                    Container(width = 10.dp, height = 10.dp) {
-                        Container(width = 10.dp, height = 10.dp) {
-                            Container(
-                                Modifier.onGloballyPositioned {
-                                    realLeft = it.positionInRoot.x
-                                    positionedLatch.countDown()
-                                },
-                                width = 10.dp,
-                                height = 10.dp
-                            ) {
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-
-        positionedLatch = CountDownLatch(1)
-        activityTestRule.runOnUiThread { left.value = 40.dp }
-
-        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-        with(density) {
-            assertThat(realLeft).isEqualTo(40.dp.toPx())
-        }
-    }
-
-    @Test
-    fun testAlignmentLinesArePresent() {
-        val latch = CountDownLatch(1)
-        val line = VerticalAlignmentLine(::min)
-        val lineValue = 10
-        show {
-            val onPositioned = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
-                Assert.assertEquals(1, coordinates.providedAlignmentLines.size)
-                Assert.assertEquals(lineValue, coordinates[line])
-                latch.countDown()
-            }
-            Layout(modifier = onPositioned, content = { }) { _, _ ->
-                layout(0, 0, mapOf(line to lineValue)) { }
-            }
-        }
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-    }
-
-    @Composable
-    private fun Offset(sizeModel: State<Dp>, content: @Composable () -> Unit) {
-        // simple copy of Padding which doesn't recompose when the size changes
-        Layout(content) { measurables, constraints ->
-            layout(constraints.maxWidth, constraints.maxHeight) {
-                measurables.first().measure(constraints)
-                    .placeRelative(sizeModel.value.toPx().roundToInt(), 0)
-            }
-        }
-    }
-}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutPaddingTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
similarity index 98%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutPaddingTest.kt
rename to compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
index 06c69bc..4f66539 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutPaddingTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
@@ -50,7 +50,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class LayoutPaddingTest : LayoutTest() {
+class PaddingTest : LayoutTest() {
 
     @Before
     fun before() {
@@ -298,7 +298,7 @@
             }
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val rootWidth = root.width
@@ -361,7 +361,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootWidth = root.width
 
@@ -451,7 +451,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val innerSize = (size - paddingPx * 2)
@@ -499,7 +499,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val paddingLeft = left.toIntPx()
@@ -553,7 +553,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(0, 0), childSize)
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
index bd823de..b9a80bf 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
@@ -109,7 +109,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(size, size), childSize[0])
@@ -161,7 +161,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootWidth = root.width
 
@@ -218,7 +218,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(childrenWidth, childrenHeight), childSize[0])
@@ -263,7 +263,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(size, size), childSize[0])
@@ -315,7 +315,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootHeight = root.height
 
@@ -369,7 +369,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(childrenWidth, childrenHeight), childSize[0])
@@ -624,7 +624,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(size, root.height), childSize[0])
@@ -682,7 +682,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootHeight = root.height
 
@@ -755,7 +755,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootHeight = root.height
 
@@ -976,7 +976,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(root.width, size), childSize[0])
@@ -1035,7 +1035,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootWidth = root.width
 
@@ -1104,7 +1104,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootWidth = root.width
 
@@ -1286,7 +1286,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1316,7 +1316,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1355,7 +1355,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1385,7 +1385,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1415,7 +1415,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1448,7 +1448,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1490,7 +1490,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1523,7 +1523,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1556,7 +1556,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1589,7 +1589,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1638,7 +1638,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1756,7 +1756,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1786,7 +1786,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1825,7 +1825,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1855,7 +1855,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1885,7 +1885,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1918,7 +1918,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1960,7 +1960,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -1993,7 +1993,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -2026,7 +2026,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -2059,7 +2059,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -2109,7 +2109,7 @@
         }
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -2244,7 +2244,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(Offset(0f, 0f), childPosition[0])
@@ -2290,7 +2290,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(Offset((root.width - size.toFloat() * 3), 0f), childPosition[0])
@@ -2336,7 +2336,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val extraSpace = root.width - size * 3
@@ -2392,7 +2392,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -2447,7 +2447,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width - size.toFloat() * 3) / 2
@@ -2500,7 +2500,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width.toFloat() - size * 3) / 3
@@ -2705,7 +2705,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(Offset(0f, 0f), childPosition[0])
@@ -2751,7 +2751,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(Offset(0f, (root.height - size.toFloat() * 3)), childPosition[0])
@@ -2797,7 +2797,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val extraSpace = root.height - size * 3f
@@ -2856,7 +2856,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.height - size.toFloat() * 3) / 4
@@ -2920,7 +2920,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.height - size.toFloat() * 3f) / 2f
@@ -2973,7 +2973,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.height - size.toFloat() * 3f) / 3f
@@ -4047,7 +4047,7 @@
         }
 
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootWidth = root.width
 
@@ -4096,7 +4096,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val extraSpace = root.width - size * 3
@@ -4152,7 +4152,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -4207,7 +4207,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width - size.toFloat() * 3) / 2
@@ -4260,7 +4260,7 @@
 
         calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width.toFloat() - size * 3) / 3
@@ -4388,7 +4388,7 @@
         }
 
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootWidth = root.width
 
@@ -4475,7 +4475,7 @@
         }
 
         assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
         val rootWidth = root.width
 
@@ -4535,7 +4535,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(Offset(0f, 0f), childPosition[0])
@@ -4590,7 +4590,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(Offset(0f, 0f), childPosition[0])
@@ -4642,7 +4642,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -4703,7 +4703,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -4761,7 +4761,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val extraSpace = root.width - size * 3
@@ -4832,7 +4832,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val extraSpace = root.width - size * 3
@@ -4900,7 +4900,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -4969,7 +4969,7 @@
                 childLayoutCoordinates
             )
 
-            val root = findOwnerView()
+            val root = findComposeView()
             waitForDraw(root)
 
             val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -5034,7 +5034,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width - size.toFloat() * 3) / 2
@@ -5101,7 +5101,7 @@
                 childLayoutCoordinates
             )
 
-            val root = findOwnerView()
+            val root = findComposeView()
             waitForDraw(root)
 
             val gap = (root.width - size.toFloat() * 3) / 2
@@ -5163,7 +5163,7 @@
             childLayoutCoordinates
         )
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         val gap = (root.width.toFloat() - size * 3) / 3
@@ -5233,7 +5233,7 @@
                 childLayoutCoordinates
             )
 
-            val root = findOwnerView()
+            val root = findComposeView()
             waitForDraw(root)
 
             val gap = (root.width.toFloat() - size * 3) / 3
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
similarity index 75%
rename from compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt
rename to compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
index 6e71283..7041c6d 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
@@ -17,6 +17,8 @@
 package androidx.compose.foundation.layout
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.emptyContent
+import androidx.compose.runtime.Providers
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
@@ -25,13 +27,18 @@
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.node.Ref
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.ValueElement
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.enforce
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
@@ -43,10 +50,11 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class LayoutSizeTest : LayoutTest() {
+class SizeTest : LayoutTest() {
 
     @Before
     fun before() {
@@ -1283,10 +1291,16 @@
         expectedConstraints: Constraints
     ) {
         val latch = CountDownLatch(1)
+        // Capture constraints and assert on test thread
+        var actualConstraints: Constraints? = null
+        // Clear contents before each test so that we don't recompose the WithConstraints call;
+        // doing so would recompose the old subcomposition with old constraints in the presence of
+        // new content before the measurement performs explicit composition the new constraints.
+        show(emptyContent())
         show {
             Layout({
                 WithConstraints(modifier) {
-                    assertEquals(expectedConstraints, constraints)
+                    actualConstraints = constraints
                     latch.countDown()
                 }
             }) { measurables, _ ->
@@ -1295,6 +1309,7 @@
             }
         }
         assertTrue(latch.await(1, TimeUnit.SECONDS))
+        assertEquals(expectedConstraints, actualConstraints)
     }
 
     private fun verifyIntrinsicMeasurements(expandedModifier: Modifier) = with(density) {
@@ -1322,4 +1337,411 @@
             assertEquals(40, maxIntrinsicHeight(Constraints.Infinity))
         }
     }
+    @Test
+    fun test2DWrapContentSize() = with(density) {
+        val sizeDp = 50.dp
+        val size = sizeDp.toIntPx()
+
+        val positionedLatch = CountDownLatch(2)
+        val alignSize = Ref<IntSize>()
+        val alignPosition = Ref<Offset>()
+        val childSize = Ref<IntSize>()
+        val childPosition = Ref<Offset>()
+        show {
+            Container(Modifier.saveLayoutInfo(alignSize, alignPosition, positionedLatch)) {
+                Container(
+                    Modifier.fillMaxSize()
+                        .wrapContentSize(Alignment.BottomEnd)
+                        .preferredSize(sizeDp)
+                        .saveLayoutInfo(childSize, childPosition, positionedLatch)
+                ) {
+                }
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        val root = findComposeView()
+        waitForDraw(root)
+
+        assertEquals(IntSize(root.width, root.height), alignSize.value)
+        assertEquals(Offset(0f, 0f), alignPosition.value)
+        assertEquals(IntSize(size, size), childSize.value)
+        assertEquals(
+            Offset(root.width - size.toFloat(), root.height - size.toFloat()),
+            childPosition.value
+        )
+    }
+
+    @Test
+    fun test1DWrapContentSize() = with(density) {
+        val sizeDp = 50.dp
+        val size = sizeDp.toIntPx()
+
+        val positionedLatch = CountDownLatch(2)
+        val alignSize = Ref<IntSize>()
+        val alignPosition = Ref<Offset>()
+        val childSize = Ref<IntSize>()
+        val childPosition = Ref<Offset>()
+        show {
+            Container(
+                Modifier.saveLayoutInfo(
+                    size = alignSize,
+                    position = alignPosition,
+                    positionedLatch = positionedLatch
+                )
+            ) {
+                Container(
+                    Modifier.fillMaxSize()
+                        .wrapContentWidth(Alignment.End)
+                        .preferredWidth(sizeDp)
+                        .saveLayoutInfo(childSize, childPosition, positionedLatch)
+                ) {
+                }
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        val root = findComposeView()
+        waitForDraw(root)
+
+        assertEquals(IntSize(root.width, root.height), alignSize.value)
+        assertEquals(Offset(0f, 0f), alignPosition.value)
+        assertEquals(IntSize(size, root.height), childSize.value)
+        assertEquals(Offset(root.width - size.toFloat(), 0f), childPosition.value)
+    }
+
+    @Test
+    fun testWrapContentSize_rtl() = with(density) {
+        val sizeDp = 200.toDp()
+        val size = sizeDp.toIntPx()
+
+        val positionedLatch = CountDownLatch(3)
+        val childSize = Array(3) { Ref<IntSize>() }
+        val childPosition = Array(3) { Ref<Offset>() }
+        show {
+            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                Box(Modifier.fillMaxSize()) {
+                    Box(Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+                        Box(
+                            Modifier.preferredSize(sizeDp)
+                                .saveLayoutInfo(childSize[0], childPosition[0], positionedLatch)
+                        ) {
+                        }
+                    }
+                    Box(Modifier.fillMaxSize().wrapContentHeight(Alignment.CenterVertically)) {
+                        Box(
+                            Modifier.preferredSize(sizeDp)
+                                .saveLayoutInfo(childSize[1], childPosition[1], positionedLatch)
+                        ) {
+                        }
+                    }
+                    Box(Modifier.fillMaxSize().wrapContentSize(Alignment.BottomEnd)) {
+                        Box(
+                            Modifier.preferredSize(sizeDp)
+                                .saveLayoutInfo(childSize[2], childPosition[2], positionedLatch)
+                        ) {
+                        }
+                    }
+                }
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        val root = findComposeView()
+        waitForDraw(root)
+
+        assertEquals(
+            Offset((root.width - size).toFloat(), 0f),
+            childPosition[0].value
+        )
+        assertEquals(
+            Offset(
+                (root.width - size).toFloat(),
+                ((root.height - size) / 2).toFloat()
+            ),
+            childPosition[1].value
+        )
+        assertEquals(
+            Offset(0f, (root.height - size).toFloat()),
+            childPosition[2].value
+        )
+    }
+
+    @Test
+    fun testModifier_wrapsContent() = with(density) {
+        val contentSize = 50.dp
+        val size = Ref<IntSize>()
+        val latch = CountDownLatch(1)
+        show {
+            Container {
+                Container(Modifier.saveLayoutInfo(size, Ref(), latch)) {
+                    Container(
+                        Modifier.wrapContentSize(Alignment.TopStart)
+                            .preferredSize(contentSize)
+                    ) {}
+                }
+            }
+        }
+
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+        assertEquals(IntSize(contentSize.toIntPx(), contentSize.toIntPx()), size.value)
+    }
+
+    @Test
+    fun testWrapContentSize_wrapsContent_whenMeasuredWithInfiniteConstraints() = with(density) {
+        val sizeDp = 50.dp
+        val size = sizeDp.toIntPx()
+
+        val positionedLatch = CountDownLatch(2)
+        val alignSize = Ref<IntSize>()
+        val alignPosition = Ref<Offset>()
+        val childSize = Ref<IntSize>()
+        val childPosition = Ref<Offset>()
+        show {
+            Layout(
+                content = {
+                    Container(
+                        Modifier.saveLayoutInfo(alignSize, alignPosition, positionedLatch)
+                    ) {
+                        Container(
+                            Modifier.wrapContentSize(Alignment.BottomEnd)
+                                .preferredSize(sizeDp)
+                                .saveLayoutInfo(childSize, childPosition, positionedLatch)
+                        ) {
+                        }
+                    }
+                },
+                measureBlock = { measurables, constraints ->
+                    val placeable = measurables.first().measure(Constraints())
+                    layout(constraints.maxWidth, constraints.maxHeight) {
+                        placeable.placeRelative(0, 0)
+                    }
+                }
+            )
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        val root = findComposeView()
+        waitForDraw(root)
+
+        assertEquals(IntSize(size, size), alignSize.value)
+        assertEquals(Offset(0f, 0f), alignPosition.value)
+        assertEquals(IntSize(size, size), childSize.value)
+        assertEquals(Offset(0f, 0f), childPosition.value)
+    }
+
+    @Test
+    fun testWrapContentSize_respectsMinConstraints() = with(density) {
+        val sizeDp = 50.dp
+        val size = sizeDp.toIntPx()
+        val doubleSizeDp = sizeDp * 2
+        val doubleSize = doubleSizeDp.toIntPx()
+
+        val positionedLatch = CountDownLatch(2)
+        val wrapSize = Ref<IntSize>()
+        val childSize = Ref<IntSize>()
+        val childPosition = Ref<Offset>()
+        show {
+            Container(Modifier.wrapContentSize(Alignment.TopStart)) {
+                Layout(
+                    modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
+                        wrapSize.value = coordinates.size
+                        positionedLatch.countDown()
+                    },
+                    content = {
+                        Container(
+                            Modifier.wrapContentSize(Alignment.Center)
+                                .preferredSize(sizeDp)
+                                .saveLayoutInfo(childSize, childPosition, positionedLatch)
+                        ) {
+                        }
+                    },
+                    measureBlock = { measurables, incomingConstraints ->
+                        val measurable = measurables.first()
+                        val constraints = Constraints(
+                            minWidth = doubleSizeDp.toIntPx(),
+                            minHeight = doubleSizeDp.toIntPx()
+                        ).enforce(incomingConstraints)
+                        val placeable = measurable.measure(constraints)
+                        layout(placeable.width, placeable.height) {
+                            placeable.placeRelative(IntOffset.Zero)
+                        }
+                    }
+                )
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        assertEquals(IntSize(doubleSize, doubleSize), wrapSize.value)
+        assertEquals(IntSize(size, size), childSize.value)
+        assertEquals(
+            Offset(
+                ((doubleSize - size) / 2f).roundToInt().toFloat(),
+                ((doubleSize - size) / 2f).roundToInt().toFloat()
+            ),
+            childPosition.value
+        )
+    }
+
+    @Test
+    fun testWrapContentSize_unbounded() = with(density) {
+        val outerSize = 10f
+        val innerSize = 20f
+
+        val positionedLatch = CountDownLatch(4)
+        show {
+            Box(
+                Modifier.size(outerSize.toDp())
+                    .onGloballyPositioned {
+                        assertEquals(outerSize, it.size.width.toFloat())
+                        positionedLatch.countDown()
+                    }
+            ) {
+                Box(
+                    Modifier.wrapContentSize(Alignment.BottomEnd, unbounded = true)
+                        .size(innerSize.toDp())
+                        .onGloballyPositioned {
+                            assertEquals(
+                                Offset(outerSize - innerSize, outerSize - innerSize),
+                                it.positionInParent
+                            )
+                            positionedLatch.countDown()
+                        }
+                )
+                Box(
+                    Modifier.wrapContentWidth(Alignment.End, unbounded = true)
+                        .size(innerSize.toDp())
+                        .onGloballyPositioned {
+                            assertEquals(outerSize - innerSize, it.positionInParent.x)
+                            positionedLatch.countDown()
+                        }
+                )
+                Box(
+                    Modifier.wrapContentHeight(Alignment.Bottom, unbounded = true)
+                        .size(innerSize.toDp())
+                        .onGloballyPositioned {
+                            assertEquals(outerSize - innerSize, it.positionInParent.y)
+                            positionedLatch.countDown()
+                        }
+                )
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+    }
+
+    @Test
+    fun test2DAlignedModifier_hasCorrectIntrinsicMeasurements() = with(density) {
+        testIntrinsics(
+            @Composable {
+                Container(Modifier.wrapContentSize(Alignment.TopStart).aspectRatio(2f)) { }
+            }
+        ) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight ->
+            // Min width.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals(25.dp.toIntPx() * 2, minIntrinsicWidth(25.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), minIntrinsicWidth(Constraints.Infinity))
+
+            // Min height.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), minIntrinsicHeight(50.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), minIntrinsicHeight(Constraints.Infinity))
+
+            // Max width.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals(25.dp.toIntPx() * 2, maxIntrinsicWidth(25.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), maxIntrinsicWidth(Constraints.Infinity))
+
+            // Max height.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), maxIntrinsicHeight(50.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), maxIntrinsicHeight(Constraints.Infinity))
+        }
+    }
+
+    @Test
+    fun test1DAlignedModifier_hasCorrectIntrinsicMeasurements() = with(density) {
+        testIntrinsics({
+            Container(
+                Modifier.wrapContentHeight(Alignment.CenterVertically)
+                    .aspectRatio(2f)
+            ) { }
+        }) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight ->
+
+            // Min width.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals(25.dp.toIntPx() * 2, minIntrinsicWidth(25.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), minIntrinsicWidth(Constraints.Infinity))
+
+            // Min height.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), minIntrinsicHeight(50.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), minIntrinsicHeight(Constraints.Infinity))
+
+            // Max width.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals(25.dp.toIntPx() * 2, maxIntrinsicWidth(25.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), maxIntrinsicWidth(Constraints.Infinity))
+
+            // Max height.
+            assertEquals(0, minIntrinsicWidth(0))
+            assertEquals((50.dp.toIntPx() / 2f).roundToInt(), maxIntrinsicHeight(50.dp.toIntPx()))
+            assertEquals(0.dp.toIntPx(), maxIntrinsicHeight(Constraints.Infinity))
+        }
+    }
+
+    @Test
+    fun testAlignedModifier_alignsCorrectly_whenOddDimensions_endAligned() = with(density) {
+        // Given a 100 x 100 pixel container, we want to make sure that when aligning a 1 x 1 pixel
+        // child to both ends (bottom, and right) we correctly position children at the last
+        // possible pixel, and avoid rounding issues. Previously we first centered the coordinates,
+        // and then aligned after, so the maths would actually be (99 / 2) * 2, which incorrectly
+        // ends up at 100 (Int rounds up) - so the last pixels in both directions just wouldn't
+        // be visible.
+        val parentSize = 100.toDp()
+        val childSizeDp = 1.toDp()
+        val childSizeIpx = childSizeDp.toIntPx()
+
+        val positionedLatch = CountDownLatch(2)
+        val alignSize = Ref<IntSize>()
+        val alignPosition = Ref<Offset>()
+        val childSize = Ref<IntSize>()
+        val childPosition = Ref<Offset>()
+        show {
+            Layout(
+                content = {
+                    Container(
+                        Modifier.preferredSize(parentSize)
+                            .saveLayoutInfo(alignSize, alignPosition, positionedLatch)
+                    ) {
+                        Container(
+                            Modifier.fillMaxSize()
+                                .wrapContentSize(Alignment.BottomEnd)
+                                .preferredSize(childSizeDp)
+                                .saveLayoutInfo(childSize, childPosition, positionedLatch)
+                        ) {
+                        }
+                    }
+                },
+                measureBlock = { measurables, constraints ->
+                    val placeable = measurables.first().measure(Constraints())
+                    layout(constraints.maxWidth, constraints.maxHeight) {
+                        placeable.placeRelative(0, 0)
+                    }
+                }
+            )
+        }
+        positionedLatch.await(1, TimeUnit.SECONDS)
+
+        val root = findComposeView()
+        waitForDraw(root)
+
+        assertEquals(IntSize(childSizeIpx, childSizeIpx), childSize.value)
+        assertEquals(
+            Offset(
+                (alignSize.value!!.width - childSizeIpx).toFloat(),
+                (alignSize.value!!.height - childSizeIpx).toFloat()
+            ),
+            childPosition.value
+        )
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/TextLayoutDirectionModifierTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/TextLayoutDirectionModifierTest.kt
deleted file mode 100644
index bad57a1..0000000
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/TextLayoutDirectionModifierTest.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.layout
-
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.runtime.Providers
-import androidx.compose.ui.platform.AmbientLayoutDirection
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class TextLayoutDirectionModifierTest : LayoutTest() {
-
-    @Test
-    fun test_CoreTextField_RtlLayoutDirection_changesDirectionTo_Rtl() {
-        val latch = CountDownLatch(1)
-        var layoutDirection: LayoutDirection? = null
-
-        show {
-            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-                BasicTextField(
-                    value = TextFieldValue("..."),
-                    onValueChange = {},
-                    onTextLayout = { result ->
-                        layoutDirection = result.layoutInput.layoutDirection
-                        latch.countDown()
-                    }
-                )
-            }
-        }
-
-        assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
-        assertThat(layoutDirection).isNotNull()
-        assertThat(layoutDirection!!).isEqualTo(LayoutDirection.Rtl)
-    }
-
-    @Test
-    fun test_CoreText_RtlLayoutDirection_changesDirectionTo_Rtl() {
-        val latch = CountDownLatch(1)
-        var layoutDirection: LayoutDirection? = null
-        show {
-            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-                BasicText(
-                    text = AnnotatedString("..."),
-                    style = TextStyle.Default,
-                    onTextLayout = { result ->
-                        layoutDirection = result.layoutInput.layoutDirection
-                        latch.countDown()
-                    },
-                    softWrap = true,
-                    overflow = TextOverflow.Clip,
-                    maxLines = 1,
-                    inlineContent = mapOf()
-                )
-            }
-        }
-
-        assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
-        assertThat(layoutDirection).isNotNull()
-        assertThat(layoutDirection!!).isEqualTo(LayoutDirection.Rtl)
-    }
-}
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
index 9589035..36f9646 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
@@ -27,14 +27,14 @@
 import kotlin.math.roundToInt
 
 /**
- * Used to specify the arrangement of the layout's children in [Row] or [Column] in the main axis
- * direction (horizontal and vertical, respectively).
+ * Used to specify the arrangement of the layout's children in layouts like [Row] or [Column] in
+ * the main axis direction (horizontal and vertical, respectively).
  */
 @Immutable
 @OptIn(InternalLayoutApi::class)
 object Arrangement {
     /**
-     * Used to specify the horizontal arrangement of the layout's children in a [Row].
+     * Used to specify the horizontal arrangement of the layout's children in layouts like [Row].
      */
     @InternalLayoutApi
     @Immutable
@@ -45,7 +45,7 @@
         val spacing get() = 0.dp
 
         /**
-         * Horizontally places the layout children inside the [Row].
+         * Horizontally places the layout children.
          *
          * @param totalSize Available space that can be occupied by the children.
          * @param size An array of sizes of all children.
@@ -64,7 +64,7 @@
     }
 
     /**
-     * Used to specify the vertical arrangement of the layout's children in a [Column].
+     * Used to specify the vertical arrangement of the layout's children in layouts like [Column].
      */
     @InternalLayoutApi
     @Immutable
@@ -75,7 +75,7 @@
         val spacing get() = 0.dp
 
         /**
-         * Vertically places the layout children inside the [Column].
+         * Vertically places the layout children.
          *
          * @param totalSize Available space that can be occupied by the children.
          * @param size An array of sizes of all children.
@@ -91,8 +91,9 @@
     }
 
     /**
-     * Used to specify the horizontal arrangement of the layout's children in a [Row], or
-     * the vertical arrangement of the layout's children in a [Column].
+     * Used to specify the horizontal arrangement of the layout's children in horizontal layouts
+     * like [Row], or the vertical arrangement of the layout's children in vertical layouts like
+     * [Column].
      */
     @InternalLayoutApi
     @Immutable
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutAspectRatio.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AspectRatio.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutAspectRatio.kt
rename to compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AspectRatio.kt
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
index bff5c98..44c0b52 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt
@@ -27,8 +27,7 @@
 import androidx.compose.ui.layout.MeasuringIntrinsicsMeasureBlocks
 import androidx.compose.ui.layout.ParentDataModifier
 import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.MeasureBlocks
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
 import androidx.compose.ui.platform.NoInspectorInfo
@@ -57,7 +56,6 @@
  * @param content The content of the [Box].
  */
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class)
 inline fun Box(
     modifier: Modifier = Modifier,
     contentAlignment: Alignment = Alignment.TopStart,
@@ -83,8 +81,7 @@
     }
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
-internal val DefaultBoxMeasureBlocks: LayoutNode.MeasureBlocks =
+internal val DefaultBoxMeasureBlocks: MeasureBlocks =
     boxMeasureBlocks(Alignment.TopStart)
 
 internal fun boxMeasureBlocks(alignment: Alignment) =
@@ -189,12 +186,10 @@
  * @param modifier The modifier to be applied to the layout.
  */
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class)
 fun Box(modifier: Modifier) {
     Layout({}, measureBlocks = EmptyBoxMeasureBlocks, modifier = modifier)
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal val EmptyBoxMeasureBlocks = MeasuringIntrinsicsMeasureBlocks { _, constraints ->
     layout(constraints.minWidth, constraints.minHeight) {}
 }
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
index 44482f2..7e7cb6f 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
@@ -25,7 +25,6 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.Measured
 import androidx.compose.ui.layout.VerticalAlignmentLine
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.util.annotation.FloatRange
 
@@ -66,7 +65,7 @@
  * @see [androidx.compose.foundation.lazy.LazyColumn]
  */
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class, InternalLayoutApi::class)
+@OptIn(InternalLayoutApi::class)
 inline fun Column(
     modifier: Modifier = Modifier,
     verticalArrangement: Arrangement.Vertical = Arrangement.Top,
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutOffset.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
similarity index 84%
rename from compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutOffset.kt
rename to compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
index eaf2378..51675bc 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutOffset.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
 
@@ -86,7 +87,7 @@
 )
 
 /**
- * Offset the content by ([x] px, [y] px). The offsets can be positive as well as non-positive.
+ * Offset the content by [offset] px. The offsets can be positive as well as non-positive.
  * Applying an offset only changes the position of the content, without interfering with
  * its size measurement.
  * This modifier is designed to be used for offsets that change, possibly due to user interactions.
@@ -100,34 +101,33 @@
  * Example usage:
  * @sample androidx.compose.foundation.layout.samples.OffsetPxModifier
  */
-fun Modifier.offset(
-    x: Density.() -> Float = { 0f },
-    y: Density.() -> Float = { 0f }
-) = this.then(
+fun Modifier.offset(offset: Density.() -> IntOffset) = this.then(
     OffsetPxModifier(
-        x = x,
-        y = y,
+        offset = offset,
         rtlAware = true,
         inspectorInfo = debugInspectorInfo {
             name = "offset"
-            properties["x"] = x
-            properties["y"] = y
+            properties["offset"] = offset
         }
     )
 )
 
 @Deprecated(
     "offsetPx was deprecated. Please use offset with lambda parameters instead.",
-    ReplaceWith("offset({x.value}, {y.value})", "androidx.compose.foundation.layout.offset")
+    ReplaceWith(
+        "offset({x.value}, {y.value})",
+        "androidx.compose.foundation.layout.offset",
+        "kotlin.math.roundToInt()"
+    )
 )
 @Suppress("ModifierInspectorInfo")
 fun Modifier.offsetPx(
     x: State<Float> = mutableStateOf(0f),
     y: State<Float> = mutableStateOf(0f)
-) = this.offset({ x.value }, { y.value })
+) = this.offset { IntOffset(x.value.roundToInt(), y.value.roundToInt()) }
 
 /**
- * Offset the content by ([x] px, [y] px). The offsets can be positive as well as non-positive.
+ * Offset the content by [offset] px. The offsets can be positive as well as non-positive.
  * Applying an offset only changes the position of the content, without interfering with
  * its size measurement.
  * This modifier is designed to be used for offsets that change, possibly due to user interactions.
@@ -142,17 +142,14 @@
  * @sample androidx.compose.foundation.layout.samples.AbsoluteOffsetPxModifier
  */
 fun Modifier.absoluteOffset(
-    x: Density.() -> Float = { 0f },
-    y: Density.() -> Float = { 0f }
+    offset: Density.() -> IntOffset
 ) = this.then(
     OffsetPxModifier(
-        x = x,
-        y = y,
+        offset = offset,
         rtlAware = false,
         inspectorInfo = debugInspectorInfo {
             name = "absoluteOffset"
-            properties["x"] = x
-            properties["y"] = y
+            properties["offset"] = offset
         }
     )
 )
@@ -160,15 +157,16 @@
 @Deprecated(
     "absoluteOffsetPx was deprecated. Please use absoluteOffset with lambda parameters instead.",
     ReplaceWith(
-        "absoluteOffset({x.value}, {y.value})",
-        "androidx.compose.foundation.layout.absoluteOffset"
+        "absoluteOffset({x.value.roundToInt()}, {y.value.roundToInt()})",
+        "androidx.compose.foundation.layout.absoluteOffset",
+        "kotlin.math.roundToInt"
     )
 )
 @Suppress("ModifierInspectorInfo")
 fun Modifier.absoluteOffsetPx(
     x: State<Float> = mutableStateOf(0f),
     y: State<Float> = mutableStateOf(0f)
-) = this.absoluteOffset({ x.value }, { y.value })
+) = this.absoluteOffset { IntOffset(x.value.roundToInt(), y.value.roundToInt()) }
 
 private class OffsetModifier(
     val x: Dp,
@@ -210,8 +208,7 @@
 }
 
 private class OffsetPxModifier(
-    val x: Density.() -> Float,
-    val y: Density.() -> Float,
+    val offset: Density.() -> IntOffset,
     val rtlAware: Boolean,
     inspectorInfo: InspectorInfo.() -> Unit
 ) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
@@ -221,10 +218,11 @@
     ): MeasureResult {
         val placeable = measurable.measure(constraints)
         return layout(placeable.width, placeable.height) {
+            val offsetValue = offset()
             if (rtlAware) {
-                placeable.placeRelativeWithLayer(x().roundToInt(), y().roundToInt())
+                placeable.placeRelativeWithLayer(offsetValue.x, offsetValue.y)
             } else {
-                placeable.placeWithLayer(x().roundToInt(), y().roundToInt())
+                placeable.placeWithLayer(offsetValue.x, offsetValue.y)
             }
         }
     }
@@ -233,17 +231,15 @@
         if (this === other) return true
         val otherModifier = other as? OffsetPxModifier ?: return false
 
-        return x == otherModifier.x &&
-            y == otherModifier.y &&
+        return offset == otherModifier.offset &&
             rtlAware == otherModifier.rtlAware
     }
 
     override fun hashCode(): Int {
-        var result = x.hashCode()
-        result = 31 * result + y.hashCode()
+        var result = offset.hashCode()
         result = 31 * result + rtlAware.hashCode()
         return result
     }
 
-    override fun toString(): String = "OffsetPxModifier(x=$x, y=$y, rtlAware=$rtlAware)"
+    override fun toString(): String = "OffsetPxModifier(offset=$offset, rtlAware=$rtlAware)"
 }
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutPadding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutPadding.kt
rename to compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
index 2db44b37..e1f4aec 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Measured
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.util.annotation.FloatRange
 
@@ -67,7 +66,7 @@
  * @see [androidx.compose.foundation.lazy.LazyRow]
  */
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class, InternalLayoutApi::class)
+@OptIn(InternalLayoutApi::class)
 inline fun Row(
     modifier: Modifier = Modifier,
     horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index c13016b..bfa199b 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -28,8 +28,7 @@
 import androidx.compose.ui.layout.ParentDataModifier
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.measureBlocksOf
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.MeasureBlocks
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
 import androidx.compose.ui.unit.Constraints
@@ -43,14 +42,13 @@
 import kotlin.math.sign
 
 @PublishedApi
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal fun rowColumnMeasureBlocks(
     orientation: LayoutOrientation,
     arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
     arrangementSpacing: Dp,
     crossAxisSize: SizeMode,
     crossAxisAlignment: CrossAxisAlignment
-): LayoutNode.MeasureBlocks {
+): MeasureBlocks {
     fun Placeable.mainAxisSize() =
         if (orientation == LayoutOrientation.Horizontal) width else height
 
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutSize.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
similarity index 100%
rename from compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutSize.kt
rename to compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Spacer.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Spacer.kt
index 4ee0c5f..6db4f41 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Spacer.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Spacer.kt
@@ -24,8 +24,8 @@
 import androidx.compose.ui.unit.hasFixedWidth
 
 /**
- * Component that represents an empty space layout, whose size can be defined using the [LayoutWidth],
- * [LayoutHeight] and [LayoutSize] modifiers.
+ * Component that represents an empty space layout, whose size can be defined using [Modifier.width],
+ * [Modifier.height] and [Modifier.size] modifiers.
  *
  * @sample androidx.compose.foundation.layout.samples.SpacerExample
  *
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 009324b..cda92a2 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -75,7 +75,7 @@
   }
 
   public interface IndicationInstance {
-    method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+    method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
     method public default void onDispose();
   }
 
@@ -137,7 +137,7 @@
     method public static androidx.compose.ui.Modifier verticalScroll(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState state, optional boolean enabled, optional boolean reverseScrolling);
   }
 
-  @androidx.compose.runtime.Stable public final class ScrollState {
+  @androidx.compose.runtime.Stable public final class ScrollState implements androidx.compose.foundation.gestures.Scrollable {
     ctor public ScrollState(float initial, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, androidx.compose.foundation.InteractionState? interactionState);
     method public float getMaxValue();
     method public float getValue();
@@ -224,6 +224,10 @@
     method public static void fling(androidx.compose.animation.core.AnimatedFloat, float startVelocity, androidx.compose.foundation.animation.FlingConfig config, optional kotlin.jvm.functions.Function3<? super androidx.compose.animation.core.AnimationEndReason,? super java.lang.Float,? super java.lang.Float,kotlin.Unit>? onAnimationEnd);
   }
 
+  public final class SmoothScrollKt {
+    method public static suspend Object? smoothScrollBy(androidx.compose.foundation.gestures.Scrollable, float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
+  }
+
 }
 
 package androidx.compose.foundation.gestures {
@@ -232,18 +236,18 @@
   }
 
   public final class DragGestureDetectorKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? awaitDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitHorizontalDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitHorizontalTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitVerticalDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitVerticalTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? drag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? horizontalDrag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? verticalDrag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
   }
 
   public final class DraggableKt {
@@ -251,7 +255,7 @@
   }
 
   public final class ForEachGestureKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
   public final class GestureCancellationException extends java.util.concurrent.CancellationException {
@@ -265,7 +269,7 @@
     method public static long calculatePan(androidx.compose.ui.input.pointer.PointerEvent);
     method public static float calculateRotation(androidx.compose.ui.input.pointer.PointerEvent);
     method public static float calculateZoom(androidx.compose.ui.input.pointer.PointerEvent);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onRotate, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onZoom, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onPan, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
   public interface PressGestureScope extends androidx.compose.ui.unit.Density {
@@ -277,12 +281,15 @@
     method public float scrollBy(float pixels);
   }
 
-  public final class ScrollableController {
+  public interface Scrollable {
+    method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+  }
+
+  public final class ScrollableController implements androidx.compose.foundation.gestures.Scrollable {
     ctor public ScrollableController(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> consumeScrollDelta, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, androidx.compose.foundation.InteractionState? interactionState);
     method public boolean isAnimationRunning();
     method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public void smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.jvm.functions.Function2<? super androidx.compose.animation.core.AnimationEndReason,? super java.lang.Float,kotlin.Unit> onEnd);
-    method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
     method public void stopAnimation();
     property public final boolean isAnimationRunning;
   }
@@ -293,9 +300,9 @@
   }
 
   public final class TapGestureDetectorKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
   }
 
   public final class ZoomableController {
@@ -319,19 +326,40 @@
   public final class CachingItemContentFactoryKt {
   }
 
+  public abstract sealed class GridCells {
+  }
+
+  public static final class GridCells.Adaptive extends androidx.compose.foundation.lazy.GridCells {
+    method public float getMinSize-D9Ej5fM();
+    property public final float minSize;
+  }
+
+  public static final class GridCells.Fixed extends androidx.compose.foundation.lazy.GridCells {
+    ctor public GridCells.Fixed(int count);
+    method public int getCount();
+    property public final int count;
+  }
+
   public final class LazyDslKt {
-    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
   }
 
   public final class LazyForKt {
-    method @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   public final class LazyGridKt {
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
+  }
+
+  public interface LazyGridScope {
+    method public void item(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
+    method public <T> void items(java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public <T> void itemsIndexed(java.util.List<? extends T> items, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   @androidx.compose.runtime.Stable public interface LazyItemScope {
@@ -340,26 +368,50 @@
     method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
   }
 
+  public interface LazyListItemInfo {
+    method public int getIndex();
+    method public int getOffset();
+    method public int getSize();
+    property public abstract int index;
+    property public abstract int offset;
+    property public abstract int size;
+  }
+
   public final class LazyListKt {
   }
 
+  public interface LazyListLayoutInfo {
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
+  }
+
+  public final class LazyListMeasureKt {
+  }
+
   public interface LazyListScope {
     method public void item(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method public <T> void items(java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public <T> void itemsIndexed(java.util.List<? extends T> items, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.compose.runtime.Stable public final class LazyListState {
+  @androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.Scrollable {
     ctor public LazyListState(int firstVisibleItemIndex, int firstVisibleItemScrollOffset, androidx.compose.foundation.InteractionState? interactionState, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock);
     method public int getFirstVisibleItemIndex();
     method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
     method public boolean isAnimationRunning();
     method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
     method public suspend Object? snapToItemIndex(@IntRange(from=0) int index, optional @IntRange(from=0) int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final boolean isAnimationRunning;
+    property public final androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo;
     field public static final androidx.compose.foundation.lazy.LazyListState.Companion Companion;
   }
 
@@ -462,8 +514,8 @@
   }
 
   public final class BasicTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void BasicTextField-3T4ULwA(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
-    method @androidx.compose.runtime.Composable public static void BasicTextField-kNtltWE(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
+    method @androidx.compose.runtime.Composable public static void BasicTextField-3Gdz-6o(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
+    method @androidx.compose.runtime.Composable public static void BasicTextField-UWLcGC4(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
   }
 
   public final class BasicTextKt {
@@ -472,7 +524,7 @@
   }
 
   public final class CoreTextFieldKt {
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-Q2a5TJk(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
   }
 
   public final class CoreTextKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 009324b..cda92a2 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -75,7 +75,7 @@
   }
 
   public interface IndicationInstance {
-    method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+    method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
     method public default void onDispose();
   }
 
@@ -137,7 +137,7 @@
     method public static androidx.compose.ui.Modifier verticalScroll(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState state, optional boolean enabled, optional boolean reverseScrolling);
   }
 
-  @androidx.compose.runtime.Stable public final class ScrollState {
+  @androidx.compose.runtime.Stable public final class ScrollState implements androidx.compose.foundation.gestures.Scrollable {
     ctor public ScrollState(float initial, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, androidx.compose.foundation.InteractionState? interactionState);
     method public float getMaxValue();
     method public float getValue();
@@ -224,6 +224,10 @@
     method public static void fling(androidx.compose.animation.core.AnimatedFloat, float startVelocity, androidx.compose.foundation.animation.FlingConfig config, optional kotlin.jvm.functions.Function3<? super androidx.compose.animation.core.AnimationEndReason,? super java.lang.Float,? super java.lang.Float,kotlin.Unit>? onAnimationEnd);
   }
 
+  public final class SmoothScrollKt {
+    method public static suspend Object? smoothScrollBy(androidx.compose.foundation.gestures.Scrollable, float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
+  }
+
 }
 
 package androidx.compose.foundation.gestures {
@@ -232,18 +236,18 @@
   }
 
   public final class DragGestureDetectorKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? awaitDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitHorizontalDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitHorizontalTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitVerticalDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitVerticalTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? drag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? horizontalDrag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? verticalDrag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
   }
 
   public final class DraggableKt {
@@ -251,7 +255,7 @@
   }
 
   public final class ForEachGestureKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
   public final class GestureCancellationException extends java.util.concurrent.CancellationException {
@@ -265,7 +269,7 @@
     method public static long calculatePan(androidx.compose.ui.input.pointer.PointerEvent);
     method public static float calculateRotation(androidx.compose.ui.input.pointer.PointerEvent);
     method public static float calculateZoom(androidx.compose.ui.input.pointer.PointerEvent);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onRotate, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onZoom, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onPan, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
   public interface PressGestureScope extends androidx.compose.ui.unit.Density {
@@ -277,12 +281,15 @@
     method public float scrollBy(float pixels);
   }
 
-  public final class ScrollableController {
+  public interface Scrollable {
+    method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+  }
+
+  public final class ScrollableController implements androidx.compose.foundation.gestures.Scrollable {
     ctor public ScrollableController(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> consumeScrollDelta, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, androidx.compose.foundation.InteractionState? interactionState);
     method public boolean isAnimationRunning();
     method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public void smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.jvm.functions.Function2<? super androidx.compose.animation.core.AnimationEndReason,? super java.lang.Float,kotlin.Unit> onEnd);
-    method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
     method public void stopAnimation();
     property public final boolean isAnimationRunning;
   }
@@ -293,9 +300,9 @@
   }
 
   public final class TapGestureDetectorKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
   }
 
   public final class ZoomableController {
@@ -319,19 +326,40 @@
   public final class CachingItemContentFactoryKt {
   }
 
+  public abstract sealed class GridCells {
+  }
+
+  public static final class GridCells.Adaptive extends androidx.compose.foundation.lazy.GridCells {
+    method public float getMinSize-D9Ej5fM();
+    property public final float minSize;
+  }
+
+  public static final class GridCells.Fixed extends androidx.compose.foundation.lazy.GridCells {
+    ctor public GridCells.Fixed(int count);
+    method public int getCount();
+    property public final int count;
+  }
+
   public final class LazyDslKt {
-    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
   }
 
   public final class LazyForKt {
-    method @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   public final class LazyGridKt {
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
+  }
+
+  public interface LazyGridScope {
+    method public void item(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
+    method public <T> void items(java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public <T> void itemsIndexed(java.util.List<? extends T> items, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   @androidx.compose.runtime.Stable public interface LazyItemScope {
@@ -340,26 +368,50 @@
     method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
   }
 
+  public interface LazyListItemInfo {
+    method public int getIndex();
+    method public int getOffset();
+    method public int getSize();
+    property public abstract int index;
+    property public abstract int offset;
+    property public abstract int size;
+  }
+
   public final class LazyListKt {
   }
 
+  public interface LazyListLayoutInfo {
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
+  }
+
+  public final class LazyListMeasureKt {
+  }
+
   public interface LazyListScope {
     method public void item(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method public <T> void items(java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public <T> void itemsIndexed(java.util.List<? extends T> items, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.compose.runtime.Stable public final class LazyListState {
+  @androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.Scrollable {
     ctor public LazyListState(int firstVisibleItemIndex, int firstVisibleItemScrollOffset, androidx.compose.foundation.InteractionState? interactionState, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock);
     method public int getFirstVisibleItemIndex();
     method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
     method public boolean isAnimationRunning();
     method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
     method public suspend Object? snapToItemIndex(@IntRange(from=0) int index, optional @IntRange(from=0) int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final boolean isAnimationRunning;
+    property public final androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo;
     field public static final androidx.compose.foundation.lazy.LazyListState.Companion Companion;
   }
 
@@ -462,8 +514,8 @@
   }
 
   public final class BasicTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void BasicTextField-3T4ULwA(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
-    method @androidx.compose.runtime.Composable public static void BasicTextField-kNtltWE(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
+    method @androidx.compose.runtime.Composable public static void BasicTextField-3Gdz-6o(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
+    method @androidx.compose.runtime.Composable public static void BasicTextField-UWLcGC4(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
   }
 
   public final class BasicTextKt {
@@ -472,7 +524,7 @@
   }
 
   public final class CoreTextFieldKt {
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-Q2a5TJk(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
   }
 
   public final class CoreTextKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 009324b..cda92a2 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -75,7 +75,7 @@
   }
 
   public interface IndicationInstance {
-    method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+    method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
     method public default void onDispose();
   }
 
@@ -137,7 +137,7 @@
     method public static androidx.compose.ui.Modifier verticalScroll(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState state, optional boolean enabled, optional boolean reverseScrolling);
   }
 
-  @androidx.compose.runtime.Stable public final class ScrollState {
+  @androidx.compose.runtime.Stable public final class ScrollState implements androidx.compose.foundation.gestures.Scrollable {
     ctor public ScrollState(float initial, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, androidx.compose.foundation.InteractionState? interactionState);
     method public float getMaxValue();
     method public float getValue();
@@ -224,6 +224,10 @@
     method public static void fling(androidx.compose.animation.core.AnimatedFloat, float startVelocity, androidx.compose.foundation.animation.FlingConfig config, optional kotlin.jvm.functions.Function3<? super androidx.compose.animation.core.AnimationEndReason,? super java.lang.Float,? super java.lang.Float,kotlin.Unit>? onAnimationEnd);
   }
 
+  public final class SmoothScrollKt {
+    method public static suspend Object? smoothScrollBy(androidx.compose.foundation.gestures.Scrollable, float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
+  }
+
 }
 
 package androidx.compose.foundation.gestures {
@@ -232,18 +236,18 @@
   }
 
   public final class DragGestureDetectorKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? awaitDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitHorizontalDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitHorizontalTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitVerticalDragOrCancellation-ijcpFGM(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitVerticalTouchSlopOrCancellation-qFc19kk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? drag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? horizontalDrag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public static suspend Object? verticalDrag-Pd94rOk(androidx.compose.ui.input.pointer.AwaitPointerEventScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
   }
 
   public final class DraggableKt {
@@ -251,7 +255,7 @@
   }
 
   public final class ForEachGestureKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
   public final class GestureCancellationException extends java.util.concurrent.CancellationException {
@@ -265,7 +269,7 @@
     method public static long calculatePan(androidx.compose.ui.input.pointer.PointerEvent);
     method public static float calculateRotation(androidx.compose.ui.input.pointer.PointerEvent);
     method public static float calculateZoom(androidx.compose.ui.input.pointer.PointerEvent);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onRotate, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onZoom, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onPan, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
   public interface PressGestureScope extends androidx.compose.ui.unit.Density {
@@ -277,12 +281,15 @@
     method public float scrollBy(float pixels);
   }
 
-  public final class ScrollableController {
+  public interface Scrollable {
+    method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+  }
+
+  public final class ScrollableController implements androidx.compose.foundation.gestures.Scrollable {
     ctor public ScrollableController(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> consumeScrollDelta, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock, androidx.compose.foundation.InteractionState? interactionState);
     method public boolean isAnimationRunning();
     method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public void smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.jvm.functions.Function2<? super androidx.compose.animation.core.AnimationEndReason,? super java.lang.Float,kotlin.Unit> onEnd);
-    method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
     method public void stopAnimation();
     property public final boolean isAnimationRunning;
   }
@@ -293,9 +300,9 @@
   }
 
   public final class TapGestureDetectorKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
   }
 
   public final class ZoomableController {
@@ -319,19 +326,40 @@
   public final class CachingItemContentFactoryKt {
   }
 
+  public abstract sealed class GridCells {
+  }
+
+  public static final class GridCells.Adaptive extends androidx.compose.foundation.lazy.GridCells {
+    method public float getMinSize-D9Ej5fM();
+    property public final float minSize;
+  }
+
+  public static final class GridCells.Fixed extends androidx.compose.foundation.lazy.GridCells {
+    ctor public GridCells.Fixed(int count);
+    method public int getCount();
+    property public final int count;
+  }
+
   public final class LazyDslKt {
-    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
   }
 
   public final class LazyForKt {
-    method @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   public final class LazyGridKt {
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
+  }
+
+  public interface LazyGridScope {
+    method public void item(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
+    method public <T> void items(java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public <T> void itemsIndexed(java.util.List<? extends T> items, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
   @androidx.compose.runtime.Stable public interface LazyItemScope {
@@ -340,26 +368,50 @@
     method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
   }
 
+  public interface LazyListItemInfo {
+    method public int getIndex();
+    method public int getOffset();
+    method public int getSize();
+    property public abstract int index;
+    property public abstract int offset;
+    property public abstract int size;
+  }
+
   public final class LazyListKt {
   }
 
+  public interface LazyListLayoutInfo {
+    method public int getTotalItemsCount();
+    method public int getViewportEndOffset();
+    method public int getViewportStartOffset();
+    method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
+    property public abstract int totalItemsCount;
+    property public abstract int viewportEndOffset;
+    property public abstract int viewportStartOffset;
+    property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
+  }
+
+  public final class LazyListMeasureKt {
+  }
+
   public interface LazyListScope {
     method public void item(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method public <T> void items(java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
     method public <T> void itemsIndexed(java.util.List<? extends T> items, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
   }
 
-  @androidx.compose.runtime.Stable public final class LazyListState {
+  @androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.Scrollable {
     ctor public LazyListState(int firstVisibleItemIndex, int firstVisibleItemScrollOffset, androidx.compose.foundation.InteractionState? interactionState, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock);
     method public int getFirstVisibleItemIndex();
     method public int getFirstVisibleItemScrollOffset();
+    method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
     method public boolean isAnimationRunning();
     method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
     method public suspend Object? snapToItemIndex(@IntRange(from=0) int index, optional @IntRange(from=0) int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final boolean isAnimationRunning;
+    property public final androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo;
     field public static final androidx.compose.foundation.lazy.LazyListState.Companion Companion;
   }
 
@@ -462,8 +514,8 @@
   }
 
   public final class BasicTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void BasicTextField-3T4ULwA(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
-    method @androidx.compose.runtime.Composable public static void BasicTextField-kNtltWE(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor);
+    method @androidx.compose.runtime.Composable public static void BasicTextField-3Gdz-6o(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
+    method @androidx.compose.runtime.Composable public static void BasicTextField-UWLcGC4(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long cursorColor);
   }
 
   public final class BasicTextKt {
@@ -472,7 +524,7 @@
   }
 
   public final class CoreTextFieldKt {
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-Q2a5TJk(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState? interactionState, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
   }
 
   public final class CoreTextKt {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index 33ca738..b44a23a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -35,6 +35,7 @@
         ComposableDemo("Multiple-interaction InteractionState") {
             MultipleInteractionStateSample()
         },
-        DemoCategory("Suspending Gesture Detectors", CoroutineGestureDemos)
+        DemoCategory("Suspending Gesture Detectors", CoroutineGestureDemos),
+        ComposableDemo("NestedScroll") { NestedScrollDemo() },
     )
 )
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index f804404..fc7165d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -16,10 +16,15 @@
 
 package androidx.compose.foundation.demos
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
+import androidx.compose.foundation.ScrollableColumn
+import androidx.compose.foundation.animation.smoothScrollBy
 import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ExperimentalLayout
@@ -28,16 +33,17 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyColumnFor
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
+import androidx.compose.foundation.lazy.LazyVerticalGrid
+import androidx.compose.foundation.lazy.GridCells
 import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.LazyRowFor
-import androidx.compose.foundation.lazy.LazyRowForIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.material.AmbientContentColor
@@ -49,6 +55,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.savedinstancestate.savedInstanceState
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -73,22 +80,28 @@
     ComposableDemo("Rtl list") { RtlListDemo() },
     ComposableDemo("LazyColumn DSL") { LazyColumnScope() },
     ComposableDemo("LazyRow DSL") { LazyRowScope() },
+    ComposableDemo("Arrangements") { LazyListArrangements() },
+    ComposableDemo("Reverse scroll direction") { ReverseLayout() },
+    ComposableDemo("Nested lazy lists") { NestedLazyDemo() },
+    ComposableDemo("LazyGrid") { LazyGridDemo() },
     PagingDemos
 )
 
 @Composable
 private fun LazyColumnDemo() {
-    LazyColumnFor(
-        items = listOf(
-            "Hello,", "World:", "It works!", "",
-            "this one is really long and spans a few lines for scrolling purposes",
-            "these", "are", "offscreen"
-        ) + (1..100).map { "$it" }
-    ) {
-        Text(text = it, fontSize = 80.sp)
+    LazyColumn {
+        items(
+            items = listOf(
+                "Hello,", "World:", "It works!", "",
+                "this one is really long and spans a few lines for scrolling purposes",
+                "these", "are", "offscreen"
+            ) + (1..100).map { "$it" }
+        ) {
+            Text(text = it, fontSize = 80.sp)
 
-        if (it.contains("works")) {
-            Text("You can even emit multiple components per item.")
+            if (it.contains("works")) {
+                Text("You can even emit multiple components per item.")
+            }
         }
     }
 }
@@ -104,11 +117,10 @@
             Button(modifier = buttonModifier, onClick = { numItems-- }) { Text("Remove") }
             Button(modifier = buttonModifier, onClick = { offset++ }) { Text("Offset") }
         }
-        LazyColumnFor(
-            (1..numItems).map { it + offset }.toList(),
-            Modifier.fillMaxWidth()
-        ) {
-            Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+        LazyColumn(Modifier.fillMaxWidth()) {
+            items((1..numItems).map { it + offset }.toList()) {
+                Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+            }
         }
     }
 }
@@ -174,18 +186,19 @@
                 fontSize = 20.sp
             )
         }
-        LazyColumnFor(
-            (0..1000).toList(),
+        LazyColumn(
             Modifier.fillMaxWidth(),
             state = state
         ) {
-            Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+            items((0..1000).toList()) {
+                Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+            }
         }
     }
 }
 
 @Composable
-fun Button(modifier: Modifier, onClick: () -> Unit, content: @Composable () -> Unit) {
+fun Button(modifier: Modifier = Modifier, onClick: () -> Unit, content: @Composable () -> Unit) {
     Box(
         modifier
             .clickable(onClick = onClick)
@@ -198,8 +211,10 @@
 
 @Composable
 private fun LazyRowItemsDemo() {
-    LazyRowFor(items = (1..1000).toList()) {
-        Square(it)
+    LazyRow {
+        items((1..1000).toList()) {
+            Square(it)
+        }
     }
 }
 
@@ -218,11 +233,15 @@
 private fun ListWithIndexSample() {
     val friends = listOf("Alex", "John", "Danny", "Sam")
     Column {
-        LazyRowForIndexed(friends, Modifier.fillMaxWidth()) { index, friend ->
-            Text("$friend at index $index", Modifier.padding(16.dp))
+        LazyRow(Modifier.fillMaxWidth()) {
+            itemsIndexed(friends) { index, friend ->
+                Text("$friend at index $index", Modifier.padding(16.dp))
+            }
         }
-        LazyColumnForIndexed(friends, Modifier.fillMaxWidth()) { index, friend ->
-            Text("$friend at index $index", Modifier.padding(16.dp))
+        LazyColumn(Modifier.fillMaxWidth()) {
+            itemsIndexed(friends) { index, friend ->
+                Text("$friend at index $index", Modifier.padding(16.dp))
+            }
         }
     }
 }
@@ -230,14 +249,16 @@
 @Composable
 private fun RtlListDemo() {
     Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-        LazyRowForIndexed((0..100).toList(), Modifier.fillMaxWidth()) { index, item ->
-            Text(
-                "$item",
-                Modifier
-                    .size(100.dp)
-                    .background(if (index % 2 == 0) Color.LightGray else Color.Transparent)
-                    .padding(16.dp)
-            )
+        LazyRow(Modifier.fillMaxWidth()) {
+            itemsIndexed((0..100).toList()) { index, item ->
+                Text(
+                    "$item",
+                    Modifier
+                        .size(100.dp)
+                        .background(if (index % 2 == 0) Color.LightGray else Color.Transparent)
+                        .padding(16.dp)
+                )
+            }
         }
     }
 }
@@ -245,8 +266,10 @@
 @Composable
 private fun PagerLikeDemo() {
     val pages = listOf(Color.LightGray, Color.White, Color.DarkGray)
-    LazyRowFor(pages) {
-        Spacer(Modifier.fillParentMaxSize().background(it))
+    LazyRow {
+        items(pages) {
+            Spacer(Modifier.fillParentMaxSize().background(it))
+        }
     }
 }
 
@@ -297,4 +320,205 @@
             }
         }
     }
-}
\ No newline at end of file
+}
+
+@Composable
+private fun LazyListArrangements() {
+    var count by remember { mutableStateOf(3) }
+    var arrangement by remember { mutableStateOf(6) }
+    Column {
+        Row {
+            Button(onClick = { count-- }) {
+                Text("--")
+            }
+            Button(onClick = { count++ }) {
+                Text("++")
+            }
+            Button(
+                onClick = {
+                    arrangement++
+                    if (arrangement == Arrangements.size) {
+                        arrangement = 0
+                    }
+                }
+            ) {
+                Text("Next")
+            }
+            Text("$arrangement ${Arrangements[arrangement]}")
+        }
+        Row {
+            val item = @Composable {
+                Box(
+                    Modifier
+                        .height(200.dp)
+                        .fillMaxWidth()
+                        .background(Color.Red)
+                        .border(1.dp, Color.Cyan)
+                )
+            }
+            ScrollableColumn(
+                verticalArrangement = Arrangements[arrangement],
+                modifier = Modifier.weight(1f).fillMaxHeight()
+            ) {
+                (1..count).forEach {
+                    item()
+                }
+            }
+            LazyColumn(
+                verticalArrangement = Arrangements[arrangement],
+                modifier = Modifier.weight(1f).fillMaxHeight()
+            ) {
+                items((1..count).toList()) {
+                    item()
+                }
+            }
+        }
+    }
+}
+
+private val Arrangements = listOf(
+    Arrangement.Center,
+    Arrangement.Top,
+    Arrangement.Bottom,
+    Arrangement.SpaceAround,
+    Arrangement.SpaceBetween,
+    Arrangement.SpaceEvenly,
+    Arrangement.spacedBy(40.dp),
+    Arrangement.spacedBy(40.dp, Alignment.Bottom),
+)
+
+@Composable
+fun ReverseLayout() {
+    Column {
+        val scrollState = rememberScrollState()
+        val lazyState = rememberLazyListState()
+        var count by remember { mutableStateOf(3) }
+        var reverse by remember { mutableStateOf(true) }
+        Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+            Button(onClick = { count -= 5 }) {
+                Text("--")
+            }
+            Button(onClick = { count += 5 }) {
+                Text("++")
+            }
+            Button(onClick = { reverse = !reverse }) {
+                Text("=!")
+            }
+            Text("Scroll=${scrollState.value.toInt()}")
+            Text(
+                "Lazy=${lazyState.firstVisibleItemIndex}; " +
+                    "${lazyState.firstVisibleItemScrollOffset}"
+            )
+        }
+        Row {
+            val item1 = @Composable { index: Int ->
+                Text(
+                    "$index",
+                    Modifier
+                        .height(200.dp)
+                        .fillMaxWidth()
+                        .background(Color.Red)
+                        .border(1.dp, Color.Cyan)
+                )
+            }
+            val item2 = @Composable { index: Int ->
+                Text("After $index")
+            }
+            ScrollableColumn(
+                reverseScrollDirection = reverse,
+                verticalArrangement = if (reverse) Arrangement.Bottom else Arrangement.Top,
+                scrollState = scrollState,
+                modifier = Modifier.weight(1f).fillMaxHeight()
+            ) {
+                if (reverse) {
+                    (count downTo 1).forEach {
+                        item2(it)
+                        item1(it)
+                    }
+                } else {
+                    (1..count).forEach {
+                        item1(it)
+                        item2(it)
+                    }
+                }
+            }
+            LazyColumn(
+                reverseLayout = reverse,
+                state = lazyState,
+                modifier = Modifier.weight(1f).fillMaxHeight()
+            ) {
+                items((1..count).toList()) {
+                    item1(it)
+                    item2(it)
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun NestedLazyDemo() {
+    val item = @Composable { index: Int ->
+        Box(
+            Modifier.padding(16.dp).size(200.dp).background(Color.LightGray),
+            contentAlignment = Alignment.Center
+        ) {
+            var state by savedInstanceState { 0 }
+            Button(onClick = { state++ }) {
+                Text("Index=$index State=$state")
+            }
+        }
+    }
+    LazyColumn {
+        item {
+            LazyRow {
+                items(List(100) { it }) {
+                    item(it)
+                }
+            }
+        }
+        items(List(100) { it }) {
+            item(it)
+        }
+    }
+}
+
+@Composable
+private fun LazyGridDemo() {
+    val columnModes = listOf(
+        GridCells.Fixed(3),
+        GridCells.Adaptive(minSize = 60.dp)
+    )
+    var currentMode by remember { mutableStateOf(0) }
+    Column {
+        Button(
+            modifier = Modifier.wrapContentSize(),
+            onClick = {
+                currentMode = (currentMode + 1) % columnModes.size
+            }
+        ) {
+            Text("Switch mode")
+        }
+        LazyGridForMode(columnModes[currentMode])
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun LazyGridForMode(mode: GridCells) {
+    LazyVerticalGrid(
+        cells = mode
+    ) {
+        items(
+            items = (1..100).toList()
+        ) {
+            Text(
+                text = "$it",
+                fontSize = 20.sp,
+                modifier = Modifier
+                    .background(Color.Gray.copy(alpha = (it % 10) / 10f))
+                    .padding(8.dp)
+            )
+        }
+    }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt
new file mode 100644
index 0000000..b4fdf7b
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos
+
+import androidx.compose.foundation.ScrollableColumn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun NestedScrollDemo() {
+    ScrollableColumn(
+        Modifier.fillMaxSize().background(Color.Red),
+        contentPadding = PaddingValues(30.dp)
+    ) {
+        repeat(6) { outerOuterIndex ->
+            LazyColumn(
+                modifier = Modifier.fillMaxSize().border(3.dp, Color.Black).height(350.dp)
+                    .background(Color.Yellow),
+                contentPadding = PaddingValues(60.dp)
+            ) {
+                repeat(3) { outerIndex ->
+                    item {
+                        ScrollableColumn(
+                            Modifier.fillMaxSize().border(3.dp, Color.Blue).height(150.dp)
+                                .background(Color.White),
+                            contentPadding = PaddingValues(30.dp)
+                        ) {
+                            repeat(6) { innerIndex ->
+                                Box(
+                                    Modifier
+                                        .height(38.dp)
+                                        .fillMaxWidth()
+                                        .background(Color.Magenta)
+                                        .border(2.dp, Color.Yellow),
+                                    contentAlignment = Alignment.Center
+                                ) {
+                                    Text(
+                                        text = "$outerOuterIndex : $outerIndex : $innerIndex",
+                                        fontSize = 24.sp
+                                    )
+                                }
+                            }
+                        }
+                    }
+
+                    item {
+                        Spacer(Modifier.height(5.dp))
+                    }
+                }
+            }
+            Spacer(Modifier.height(5.dp))
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
index 3076180..517d7af 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.demos
 
+import android.graphics.Matrix
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
@@ -46,13 +47,12 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.consumeAllChanges
@@ -60,9 +60,12 @@
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
+import kotlin.math.atan2
 import kotlin.math.roundToInt
+import kotlin.math.sqrt
 import kotlin.random.Random
 
 val CoroutineGestureDemos = listOf(
@@ -104,7 +107,6 @@
 /**
  * Gesture detector for tap, double-tap, and long-press.
  */
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun CoroutineTapDemo() {
     var tapHue by remember { mutableStateOf(randomHue()) }
@@ -209,7 +211,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun TouchSlopDragGestures() {
     Column {
@@ -287,7 +288,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun OrientationLockDragGestures() {
     var size by remember { mutableStateOf(IntSize.Zero) }
@@ -332,7 +332,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun Drag2DGestures() {
     var size by remember { mutableStateOf(IntSize.Zero) }
@@ -344,7 +343,7 @@
         }.fillMaxSize()
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .background(Color.Blue)
                 .size(50.dp)
                 .pointerInput {
@@ -362,24 +361,41 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun MultitouchArea(
     text: String,
     gestureDetector: suspend PointerInputScope.(
-        (angle: Float) -> Unit,
-        (zoom: Float) -> Unit,
-        (pan: Offset) -> Unit
+        (centroid: Offset, pan: Offset, zoom: Float, angle: Float) -> Unit,
     ) -> Unit
 ) {
+    val matrix by remember { mutableStateOf(Matrix()) }
     var angle by remember { mutableStateOf(0f) }
     var zoom by remember { mutableStateOf(1f) }
-    val offsetX = remember { mutableStateOf(0f) }
-    val offsetY = remember { mutableStateOf(0f) }
+    var offsetX by remember { mutableStateOf(0f) }
+    var offsetY by remember { mutableStateOf(0f) }
 
-    Box(Modifier.fillMaxSize()) {
+    Box(
+        Modifier.fillMaxSize().pointerInput {
+            gestureDetector { centroid, pan, gestureZoom, gestureAngle ->
+                val anchorX = centroid.x - size.width / 2f
+                val anchorY = centroid.y - size.height / 2f
+                matrix.postRotate(gestureAngle, anchorX, anchorY)
+                matrix.postScale(gestureZoom, gestureZoom, anchorX, anchorY)
+                matrix.postTranslate(pan.x, pan.y)
+
+                val v = FloatArray(9)
+                matrix.getValues(v)
+                offsetX = v[Matrix.MTRANS_X]
+                offsetY = v[Matrix.MTRANS_Y]
+                val scaleX = v[Matrix.MSCALE_X]
+                val skewY = v[Matrix.MSKEW_Y]
+                zoom = sqrt(scaleX * scaleX + skewY * skewY)
+                angle = atan2(v[Matrix.MSKEW_X], v[Matrix.MSCALE_X]) * (-180 / Math.PI.toFloat())
+            }
+        }
+    ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                 .graphicsLayer(
                     scaleX = zoom,
                     scaleY = zoom,
@@ -408,15 +424,6 @@
                             drawRect(color = color, topLeft = topLeft, size = rectangleSize)
                         }
                     }
-                }.pointerInput {
-                    gestureDetector(
-                        { angle += it },
-                        { zoom *= it },
-                        {
-                            offsetX.value += it.x
-                            offsetY.value += it.y
-                        }
-                    )
                 }
                 .fillMaxSize()
         )
@@ -428,17 +435,14 @@
  * This is a multi-touch gesture detector, including pan, zoom, and rotation.
  * The user can pan, zoom, and rotate once touch slop has been reached.
  */
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun MultitouchGestureDetector() {
     MultitouchArea(
         "Zoom, Pan, and Rotate"
-    ) { onRotate, onZoom, onPan ->
+    ) {
         detectMultitouchGestures(
             panZoomLock = false,
-            onRotate = onRotate,
-            onZoom = onZoom,
-            onPan = onPan
+            onGesture = it
         )
     }
 }
@@ -448,17 +452,14 @@
  * It is common to want to lean toward zoom over rotation, so this gesture detector will
  * lock into zoom if the first unless the rotation passes touch slop first.
  */
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun MultitouchLockGestureDetector() {
     MultitouchArea(
         "Zoom, Pan, and Rotate Locking to Zoom"
-    ) { onRotate, onZoom, onPan ->
+    ) {
         detectMultitouchGestures(
             panZoomLock = true,
-            onRotate = onRotate,
-            onZoom = onZoom,
-            onPan = onPan
+            onGesture = it
         )
     }
 }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt
index 183dd1e..55b2f90 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt
@@ -25,10 +25,9 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.savedinstancestate.savedInstanceState
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.graphics.Color.Companion.Black
 import androidx.compose.ui.graphics.Color.Companion.Red
@@ -37,7 +36,6 @@
 import androidx.compose.ui.unit.sp
 
 @Composable
-@OptIn(ExperimentalFocus::class)
 fun TextFieldFocusTransition() {
     val focusRequesters = List(6) { FocusRequester() }
 
@@ -51,7 +49,6 @@
     }
 }
 
-@OptIn(ExperimentalFocus::class)
 @Composable
 private fun TextFieldWithFocusRequesters(
     focusRequester: FocusRequester,
@@ -63,7 +60,7 @@
     BasicTextField(
         value = state.value,
         modifier = demoTextFieldModifiers
-            .focusObserver { color = if (it.isFocused) Red else Black }
+            .onFocusChanged { color = if (it.isFocused) Red else Black }
             .focusRequester(focusRequester),
         textStyle = TextStyle(color = color, fontSize = 32.sp),
         onValueChange = { state.value = it },
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
index 1959250..2411c77 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
@@ -22,7 +22,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ParagraphStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextIndent
 import androidx.compose.ui.text.withStyle
@@ -52,7 +52,7 @@
     val text1 = "paragraph1 paragraph1 paragraph1 paragraph1 paragraph1"
     val text2 = "paragraph2 paragraph2 paragraph2 paragraph2 paragraph2"
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             append(text1)
             withStyle(ParagraphStyle()) {
                 append(text2)
@@ -64,7 +64,7 @@
 
 @Composable
 fun TextDemoParagraphTextAlign() {
-    val annotatedString = annotatedString {
+    val annotatedString = buildAnnotatedString {
         TextAlign.values().forEach { textAlign ->
             val str = List(4) { "TextAlign.$textAlign" }.joinToString(" ")
             withStyle(ParagraphStyle(textAlign = textAlign)) {
@@ -114,7 +114,7 @@
     val text2 = "TextIndent restLine TextIndent restLine TextIndent restLine"
 
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(ParagraphStyle(textIndent = TextIndent(firstLine = 20.sp))) {
                 append(text1)
             }
@@ -131,7 +131,7 @@
     val ltrText = "Hello World! Hello World! Hello World! Hello World! Hello World!"
     val rtlText = "مرحبا بالعالم مرحبا بالعالم مرحبا بالعالم مرحبا بالعالم مرحبا بالعالم"
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(ParagraphStyle()) {
                 append(ltrText)
             }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
index 6e7724e..b4d10b0 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
@@ -104,7 +104,7 @@
 fun TagLine(tag: String) {
     Text(
         style = TextStyle(fontSize = fontSize8),
-        text = annotatedString {
+        text = buildAnnotatedString {
             append("\n")
             withStyle(
                 style = SpanStyle(
@@ -121,7 +121,7 @@
 @Composable
 fun SecondTagLine(tag: String) {
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(
                 style = SpanStyle(
                     color = Color(0xFFAAAAAA),
@@ -139,7 +139,7 @@
     // This group of text composables show different color, fontSize, fontWeight and fontStyle in
     // English.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(
                 SpanStyle(
                     color = Color(0xFFFF0000),
@@ -186,7 +186,7 @@
     // This group of text composables show different color, fontSize, fontWeight and fontStyle in
     // Chinese, Arabic, and Hindi.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(
                 style = SpanStyle(
                     color = Color(0xFFFF0000),
@@ -227,7 +227,7 @@
 fun TextDemoFontFamily() {
     // This group of text composables show different fontFamilies in English.
     Text(
-        annotatedString {
+        buildAnnotatedString {
             withStyle(
                 style = SpanStyle(
                     fontSize = fontSize8,
@@ -279,7 +279,7 @@
 fun TextDemoLetterSpacing() {
     // This group of text composables show different letterSpacing.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(fontSize = fontSize8)) {
                 append("$displayText   ")
             }
@@ -319,7 +319,7 @@
 fun TextDemoBackground() {
     // This group of text composables show different background.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(background = Color(0xFFFF0000))) {
                 append("$displayText   ")
             }
@@ -341,7 +341,7 @@
     // This group of text composables show different Locales of the same Unicode codepoint.
     val text = "\u82B1"
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(localeList = LocaleList("ja-JP"))) {
                 append("$text   ")
             }
@@ -458,7 +458,7 @@
     )
     Text(
         style = TextStyle(fontSize = fontSize8),
-        text = annotatedString {
+        text = buildAnnotatedString {
             append("text with ")
             withStyle(style = SpanStyle(shadow = shadow)) {
                 append("shadow!")
@@ -471,7 +471,7 @@
 fun TextDemoFontSizeScale() {
     Text(
         style = TextStyle(fontSize = fontSize8),
-        text = annotatedString {
+        text = buildAnnotatedString {
             for (i in 4..12 step 4) {
                 val scale = i * 0.1f
                 withStyle(style = SpanStyle(fontSize = scale.em)) {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
index 4bedcd7..9a8ecbc 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
@@ -22,7 +22,7 @@
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.VerbatimTtsAnnotation
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.intl.LocaleList
 
 @Composable
@@ -30,7 +30,7 @@
     Column {
         TagLine("Text to speech with different locales.")
         Text(
-            text = annotatedString {
+            text = buildAnnotatedString {
                 pushStyle(SpanStyle(localeList = LocaleList("en-us")))
                 append("Hello!\n")
                 pop()
@@ -55,7 +55,7 @@
 
         TagLine("VerbatimTtsAnnotation ")
         Text(
-            text = annotatedString {
+            text = buildAnnotatedString {
                 append("This word is read verbatim: ")
                 pushTtsAnnotation(VerbatimTtsAnnotation(verbatim = "hello"))
                 append("hello\n")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
index dda5da3..725ef4c 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.selection.SelectionContainer
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.intl.LocaleList
@@ -61,7 +61,7 @@
                 fontWeight = FontWeight.W200,
                 fontStyle = FontStyle.Italic
             ),
-            text = annotatedString {
+            text = buildAnnotatedString {
                 append(text = "$displayText   ")
                 append(text = "$displayTextArabic   ")
                 append(text = "$displayTextChinese   ")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
index 4442e6b..9cf591b 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
@@ -30,8 +30,8 @@
 import androidx.compose.ui.selection.SelectionContainer
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
 import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 
@@ -114,7 +114,7 @@
     Row {
         Column(Modifier.weight(1f)) {
             Text(
-                text = annotatedString {
+                text = buildAnnotatedString {
                     append("To begin, follow the")
                     withStyle(link) {
                         append(" Jetpack Compose setup instructions ")
@@ -137,7 +137,7 @@
             .background(rectColor)
     )
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(commonStyle.toSpanStyle()) {
                 append(
                     "The setContent block defines the activity's layout. Instead of " +
@@ -165,7 +165,7 @@
         style = commonStyle.merge(header2)
     )
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(commonStyle.toSpanStyle()) {
                 withStyle(commonStyle.toParagraphStyle()) {
                     append(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt
index ed21e47..68d298b 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt
@@ -16,13 +16,17 @@
 
 package androidx.compose.foundation.demos.text
 
+import androidx.compose.foundation.Interaction
+import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.ScrollableColumn
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.material.Text
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.savedinstancestate.savedInstanceState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -32,6 +36,7 @@
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.OffsetMap
 import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.text.input.TransformedText
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.text.intl.LocaleList
@@ -231,6 +236,8 @@
                 style = TextStyle(fontSize = fontSize8)
             )
         }
+        TagLine(tag = "TextField InteractionState")
+        InteractionStateTextField()
     }
 }
 
@@ -274,4 +281,24 @@
             content()
         }
     }
+}
+
+@Composable
+private fun InteractionStateTextField() {
+    val state = savedInstanceState(saver = TextFieldValue.Saver) { TextFieldValue() }
+    val interactionState = remember { InteractionState() }
+
+    Column(demoTextFieldModifiers) {
+        Text("Pressed?: ${interactionState.contains(Interaction.Pressed)}", fontSize = fontSize4)
+        Text("Focused?: ${interactionState.contains(Interaction.Focused)}", fontSize = fontSize4)
+        Text("Dragged?: ${interactionState.contains(Interaction.Dragged)}", fontSize = fontSize4)
+        BasicTextField(
+            modifier = Modifier.fillMaxWidth(),
+            value = state.value,
+            singleLine = true,
+            interactionState = interactionState,
+            onValueChange = { state.value = it },
+            textStyle = TextStyle(fontSize = fontSize8)
+        )
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BorderSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BorderSamples.kt
index 190bacc..304227b 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BorderSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BorderSamples.kt
@@ -25,8 +25,8 @@
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.HorizontalGradient
 import androidx.compose.ui.graphics.TileMode
 import androidx.compose.ui.unit.dp
 
@@ -42,7 +42,7 @@
 @Composable
 @Sampled
 fun BorderSampleWithBrush() {
-    val gradientBrush = HorizontalGradient(
+    val gradientBrush = Brush.horizontalGradient(
         colors = listOf(Color.Red, Color.Blue, Color.Green),
         startX = 0.0f,
         endX = 500.0f,
@@ -63,7 +63,6 @@
         modifier = Modifier.border(
             border = BorderStroke(2.dp, Color.Blue),
             shape = CutCornerShape(8.dp)
-        )
-            .padding(10.dp)
+        ).padding(10.dp)
     )
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt
index 4cd2ade..1dc203b 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt
@@ -48,16 +48,16 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.consumePositionChange
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
+import kotlin.math.roundToInt
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun AwaitHorizontalDragOrCancellationSample() {
@@ -69,13 +69,13 @@
             .onSizeChanged { width = it.width.toFloat() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .fillMaxHeight()
                 .width(50.dp)
                 .background(Color.Blue)
                 .pointerInput {
                     forEachGesture {
-                        handlePointerInput {
+                        awaitPointerEventScope {
                             val down = awaitFirstDown()
                             var change =
                                 awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
@@ -102,7 +102,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun HorizontalDragSample() {
@@ -114,13 +113,13 @@
             .onSizeChanged { width = it.width.toFloat() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .fillMaxHeight()
                 .width(50.dp)
                 .background(Color.Blue)
                 .pointerInput {
                     forEachGesture {
-                        handlePointerInput {
+                        awaitPointerEventScope {
                             val down = awaitFirstDown()
                             val change =
                                 awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
@@ -146,7 +145,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectHorizontalDragGesturesSample() {
@@ -158,7 +156,7 @@
             .onSizeChanged { width = it.width.toFloat() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .fillMaxHeight()
                 .width(50.dp)
                 .background(Color.Blue)
@@ -174,7 +172,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun AwaitVerticalDragOrCancellationSample() {
@@ -186,13 +183,13 @@
             .onSizeChanged { height = it.height.toFloat() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .fillMaxWidth()
                 .height(50.dp)
                 .background(Color.Blue)
                 .pointerInput {
                     forEachGesture {
-                        handlePointerInput {
+                        awaitPointerEventScope {
                             val down = awaitFirstDown()
                             var change =
                                 awaitVerticalTouchSlopOrCancellation(down.id) { change, over ->
@@ -219,7 +216,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun VerticalDragSample() {
@@ -231,13 +227,13 @@
             .onSizeChanged { height = it.height.toFloat() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .fillMaxWidth()
                 .height(50.dp)
                 .background(Color.Blue)
                 .pointerInput {
                     forEachGesture {
-                        handlePointerInput {
+                        awaitPointerEventScope {
                             val down = awaitFirstDown()
                             val change =
                                 awaitVerticalTouchSlopOrCancellation(down.id) { change, over ->
@@ -263,7 +259,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectVerticalDragGesturesSample() {
@@ -275,7 +270,7 @@
             .onSizeChanged { height = it.height.toFloat() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .fillMaxWidth()
                 .height(50.dp)
                 .background(Color.Blue)
@@ -291,7 +286,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun AwaitDragOrCancellationSample() {
@@ -303,12 +297,12 @@
             .onSizeChanged { size = it.toSize() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .size(50.dp)
                 .background(Color.Blue)
                 .pointerInput {
                     forEachGesture {
-                        handlePointerInput {
+                        awaitPointerEventScope {
                             val down = awaitFirstDown()
                             var change = awaitTouchSlopOrCancellation(down.id) { change, over ->
                                 val original = Offset(offsetX.value, offsetY.value)
@@ -348,7 +342,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DragSample() {
@@ -360,12 +353,12 @@
             .onSizeChanged { size = it.toSize() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .size(50.dp)
                 .background(Color.Blue)
                 .pointerInput {
                     forEachGesture {
-                        handlePointerInput {
+                        awaitPointerEventScope {
                             val down = awaitFirstDown()
                             val change = awaitTouchSlopOrCancellation(down.id) { change, over ->
                                 val original = Offset(offsetX.value, offsetY.value)
@@ -404,7 +397,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectDragGesturesSample() {
@@ -416,7 +408,7 @@
             .onSizeChanged { size = it.toSize() }
     ) {
         Box(
-            Modifier.offset({ offsetX.value }, { offsetY.value })
+            Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
                 .size(50.dp)
                 .background(Color.Blue)
                 .pointerInput {
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DraggableSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DraggableSamples.kt
index ca8c36c..5cc2a57 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DraggableSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DraggableSamples.kt
@@ -29,7 +29,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
 
 @Sampled
 @Composable
@@ -52,7 +54,9 @@
             .background(Color.Black)
     ) {
         Box(
-            Modifier.offset(x = { offsetPosition.value }).preferredSize(50.dp).background(Color.Red)
+            Modifier.offset { IntOffset(offsetPosition.value.roundToInt(), 0) }
+                .preferredSize(50.dp)
+                .background(Color.Red)
         )
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DrawBackgroundSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DrawBackgroundSamples.kt
index d0686b5..b876f7c 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DrawBackgroundSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DrawBackgroundSamples.kt
@@ -23,8 +23,8 @@
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.HorizontalGradient
 import androidx.compose.ui.unit.dp
 
 @Composable
@@ -39,7 +39,7 @@
 @Composable
 @Sampled
 fun DrawBackgroundShapedBrush() {
-    val gradientBrush = HorizontalGradient(
+    val gradientBrush = Brush.horizontalGradient(
         colors = listOf(Color.Red, Color.Blue, Color.Green),
         startX = 0.0f,
         endX = 500.0f
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt
index 66cd360..b5fded6 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt
@@ -26,13 +26,11 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focusRequester
 
 @Sampled
 @Composable
-@OptIn(ExperimentalFocus::class)
 fun FocusableSample() {
     // initialize focus requester to be able to request focus programmatically
     val requester = FocusRequester()
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
index fe4bcdc..58b2dca 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
@@ -28,14 +28,14 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.Placeholder
 import androidx.compose.ui.text.PlaceholderVerticalAlign
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.em
 
 @Sampled
 @Composable
 fun InlineTextContentSample() {
     val myId = "inlineContent"
-    val text = annotatedString {
+    val text = buildAnnotatedString {
         append("Hello")
         // Append a placeholder string "[myBox]" and attach an annotation "inlineContent" on it.
         appendInlineContent(myId, "[myBox]")
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyForSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyForSamples.kt
deleted file mode 100644
index d1b87dd..0000000
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyForSamples.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.samples
-
-import androidx.annotation.Sampled
-import androidx.compose.foundation.lazy.LazyColumnFor
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
-import androidx.compose.foundation.lazy.LazyRowFor
-import androidx.compose.foundation.lazy.LazyRowForIndexed
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-
-@Sampled
-@Composable
-fun LazyColumnForSample() {
-    val items = listOf("A", "B", "C")
-    LazyColumnFor(items) {
-        Text("Item is $it")
-    }
-}
-
-@Sampled
-@Composable
-fun LazyRowForSample() {
-    val items = listOf("A", "B", "C")
-    LazyRowFor(items) {
-        Text("Item is $it")
-    }
-}
-
-@Sampled
-@Composable
-fun LazyColumnForIndexedSample() {
-    val items = listOf("A", "B", "C")
-    LazyColumnForIndexed(items) { index, item ->
-        Text("Item at index $index is $item")
-    }
-}
-
-@Sampled
-@Composable
-fun LazyRowForIndexedSample() {
-    val items = listOf("A", "B", "C")
-    LazyRowForIndexed(items) { index, item ->
-        Text("Item at index $index is $item")
-    }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt
index 8410bae..ff1a5a3 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt
@@ -37,30 +37,34 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.IntOffset
+import kotlin.math.roundToInt
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectMultitouchGestures() {
     var angle by remember { mutableStateOf(0f) }
     var zoom by remember { mutableStateOf(1f) }
-    val offsetX = remember { mutableStateOf(0f) }
-    val offsetY = remember { mutableStateOf(0f) }
+    var offsetX by remember { mutableStateOf(0f) }
+    var offsetY by remember { mutableStateOf(0f) }
     Box(
-        Modifier.offset({ offsetX.value }, { offsetY.value })
-            .graphicsLayer(scaleX = zoom, scaleY = zoom, rotationZ = angle)
+        Modifier.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
+            .graphicsLayer(
+                scaleX = zoom,
+                scaleY = zoom,
+                rotationZ = angle
+            )
             .background(Color.Blue)
             .pointerInput {
                 detectMultitouchGestures(
-                    onRotate = { angle += it },
-                    onZoom = { zoom *= it },
-                    onPan = {
-                        offsetX.value += it.x
-                        offsetY.value += it.y
+                    onGesture = { _, pan, gestureZoom, gestureRotate ->
+                        angle += gestureRotate
+                        zoom *= gestureZoom
+                        offsetX += pan.x
+                        offsetY += pan.y
                     }
                 )
             }
@@ -68,7 +72,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculateRotation() {
@@ -79,7 +82,7 @@
             .background(Color.Blue)
             .pointerInput {
                 forEachGesture {
-                    handlePointerInput {
+                    awaitPointerEventScope {
                         awaitFirstDown()
                         do {
                             val event = awaitPointerEvent()
@@ -93,7 +96,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculateZoom() {
@@ -104,7 +106,7 @@
             .background(Color.Blue)
             .pointerInput {
                 forEachGesture {
-                    handlePointerInput {
+                    awaitPointerEventScope {
                         awaitFirstDown()
                         do {
                             val event = awaitPointerEvent()
@@ -117,7 +119,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculatePan() {
@@ -125,12 +126,12 @@
     val offsetY = remember { mutableStateOf(0f) }
     Box(
         Modifier
-            .offset({ offsetX.value }, { offsetY.value })
+            .offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
             .graphicsLayer()
             .background(Color.Blue)
             .pointerInput {
                 forEachGesture {
-                    handlePointerInput {
+                    awaitPointerEventScope {
                         awaitFirstDown()
                         do {
                             val event = awaitPointerEvent()
@@ -145,7 +146,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculateCentroidSize() {
@@ -159,7 +159,7 @@
             }
             .pointerInput {
                 forEachGesture {
-                    handlePointerInput {
+                    awaitPointerEventScope {
                         awaitFirstDown().also {
                             position = it.current.position
                         }
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollerSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollerSamples.kt
index e2c75d2..e3aa95b 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollerSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollerSamples.kt
@@ -36,10 +36,9 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.HorizontalGradient
 import androidx.compose.ui.graphics.TileMode
-import androidx.compose.ui.graphics.VerticalGradient
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 
@@ -88,7 +87,7 @@
 @Composable
 fun HorizontalScrollSample() {
     val scrollState = rememberScrollState()
-    val gradient = HorizontalGradient(
+    val gradient = Brush.horizontalGradient(
         listOf(Color.Red, Color.Blue, Color.Green), 0.0f, 10000.0f, TileMode.Repeated
     )
     Box(
@@ -103,7 +102,7 @@
 @Composable
 fun VerticalScrollExample() {
     val scrollState = rememberScrollState()
-    val gradient = VerticalGradient(
+    val gradient = Brush.verticalGradient(
         listOf(Color.Red, Color.Blue, Color.Green), 0.0f, 10000.0f, TileMode.Repeated
     )
     Box(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index d113774..86c30b6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.platform.InspectableValue
@@ -47,7 +46,6 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFocus::class)
 class FocusableTest {
 
     @get:Rule
@@ -98,8 +96,7 @@
 
     @Test
     fun focusableTest_focusAcquire() {
-        val requester = FocusRequester()
-        val otherRequester = FocusRequester()
+        val (requester, otherRequester) = FocusRequester.createRefs()
         rule.setContent {
             Box {
                 BasicText(
@@ -139,8 +136,7 @@
     @Test
     fun focusableTest_interactionState() {
         val interactionState = InteractionState()
-        val requester = FocusRequester()
-        val otherRequester = FocusRequester()
+        val (requester, otherRequester) = FocusRequester.createRefs()
         rule.setContent {
             Box {
                 BasicText(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
index 64dc751..d092269 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
@@ -23,6 +23,8 @@
 import androidx.compose.foundation.layout.preferredSizeIn
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.test.R
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
@@ -50,8 +52,13 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.res.imageResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.test.performClick
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import org.junit.Assert
@@ -296,6 +303,7 @@
     }
 
     @Test
+    @LargeTest
     fun testImageScalesNonuniformly() {
         val imageComposableWidth = imageWidth * 3
         val imageComposableHeight = imageHeight * 7
@@ -516,4 +524,41 @@
             Assert.assertEquals(Color.Red.toArgb(), getPixel(50, height - 1))
         }
     }
+
+    @Test
+    @LargeTest
+    fun testPainterResourceWithImage() {
+        val testTag = "testTag"
+        var imageColor = Color.Black
+
+        rule.setContent {
+            val painterId = remember {
+                mutableStateOf(R.drawable.ic_vector_square_asset_test)
+            }
+            with(imageResource(R.drawable.ic_image_test)) {
+                // Sample the actual color of the image to make sure we are loading it properly
+                // when we toggle the state of the resource id
+                imageColor = toPixelMap()[width / 2, height / 2]
+            }
+            Image(
+                painterResource(painterId.value),
+                contentScale = ContentScale.FillBounds,
+                modifier = Modifier.testTag(testTag).clickable {
+                    if (painterId.value == R.drawable.ic_vector_square_asset_test) {
+                        painterId.value = R.drawable.ic_image_test
+                    } else {
+                        painterId.value = R.drawable.ic_vector_square_asset_test
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Red }
+
+        rule.onNodeWithTag(testTag).performClick()
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { imageColor }
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
index ea79178..509ba73 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
@@ -19,7 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.InspectableValue
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index d4a9fa4..b163cd1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -21,6 +21,8 @@
 import androidx.compose.animation.core.ManualFrameClock
 import androidx.compose.animation.core.advanceClockMillis
 import androidx.compose.foundation.animation.FlingConfig
+import androidx.compose.foundation.animation.smoothScrollBy
+import androidx.compose.foundation.gestures.Scrollable
 import androidx.compose.foundation.gestures.ScrollableController
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
@@ -32,6 +34,10 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
@@ -48,6 +54,7 @@
 import androidx.compose.ui.test.swipe
 import androidx.compose.ui.test.swipeWithVelocity
 import androidx.compose.ui.test.up
+import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.milliseconds
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -435,10 +442,10 @@
         rule.awaitIdle()
         assertThat(total).isEqualTo(0f)
 
-        controller.smoothScrollBy(1000f)
+        (controller as Scrollable).smoothScrollBy(1000f)
         assertThat(total).isWithin(0.001f).of(1000f)
 
-        controller.smoothScrollBy(-200f)
+        (controller as Scrollable).smoothScrollBy(-200f)
         assertThat(total).isWithin(0.001f).of(800f)
     }
 
@@ -511,7 +518,6 @@
                 Box(
                     contentAlignment = Alignment.Center,
                     modifier = Modifier
-                        .testTag(scrollableBoxTag)
                         .preferredSize(300.dp)
                         .scrollable(
                             controller = outerState,
@@ -519,15 +525,89 @@
                         )
                 ) {
                     Box(
-                        modifier = Modifier.preferredSize(300.dp).scrollable(
-                            controller = innerState,
-                            orientation = Orientation.Horizontal
-                        )
+                        modifier = Modifier.testTag(scrollableBoxTag)
+                            .preferredSize(300.dp)
+                            .scrollable(
+                                controller = innerState,
+                                orientation = Orientation.Horizontal
+                            )
                     )
                 }
             }
         }
         rule.onNodeWithTag(scrollableBoxTag).performGesture {
+            this.swipeWithVelocity(
+                start = this.center,
+                end = Offset(this.center.x + 200f, this.center.y),
+                duration = 300.milliseconds,
+                endVelocity = 0f
+            )
+        }
+        val lastEqualDrag = rule.runOnIdle {
+            assertThat(innerDrag).isGreaterThan(0f)
+            assertThat(outerDrag).isGreaterThan(0f)
+            // we consumed half delta in child, so exactly half should go to the parent
+            assertThat(outerDrag).isEqualTo(innerDrag)
+            innerDrag
+        }
+        advanceClockWhileAwaitersExist(clock)
+        advanceClockWhileAwaitersExist(clock)
+        rule.runOnIdle {
+            // values should be the same since no fling
+            assertThat(innerDrag).isEqualTo(lastEqualDrag)
+            assertThat(outerDrag).isEqualTo(lastEqualDrag)
+        }
+    }
+
+    @Test
+    @OptIn(ExperimentalTesting::class)
+    fun scrollable_nestedFling() = runBlockingWithManualClock { clock ->
+        var innerDrag = 0f
+        var outerDrag = 0f
+        val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+        val outerState = ScrollableController(
+            consumeScrollDelta = {
+                outerDrag += it
+                it
+            },
+            flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+            animationClock = animationClock
+        )
+        val innerState = ScrollableController(
+            consumeScrollDelta = {
+                innerDrag += it / 2
+                it / 2
+            },
+            flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+            animationClock = animationClock
+        )
+
+        rule.setContent {
+            Box {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier
+                        .preferredSize(300.dp)
+                        .scrollable(
+                            controller = outerState,
+                            orientation = Orientation.Horizontal
+                        )
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .testTag(scrollableBoxTag)
+                            .preferredSize(300.dp)
+                            .scrollable(
+                                controller = innerState,
+                                orientation = Orientation.Horizontal
+                            )
+                    )
+                }
+            }
+        }
+
+        // swipe again with velocity
+        rule.onNodeWithTag(scrollableBoxTag).performGesture {
             this.swipe(
                 start = this.center,
                 end = Offset(this.center.x + 200f, this.center.y),
@@ -541,16 +621,234 @@
             assertThat(outerDrag).isEqualTo(innerDrag)
             innerDrag
         }
+        // advance clocks, triggering fling
         advanceClockWhileAwaitersExist(clock)
         advanceClockWhileAwaitersExist(clock)
-        // and nothing should change as we don't do nested fling
         rule.runOnIdle {
-            assertThat(outerDrag).isEqualTo(lastEqualDrag)
+            assertThat(innerDrag).isGreaterThan(lastEqualDrag)
+            assertThat(outerDrag).isGreaterThan(lastEqualDrag)
         }
     }
 
     @Test
     @OptIn(ExperimentalTesting::class)
+    fun scrollable_nestedScrollAbove_respectsPreConsumption() =
+        runBlockingWithManualClock { clock ->
+            var value = 0f
+            var lastReceivedPreScrollAvailable = 0f
+            val preConsumeFraction = 0.7f
+            val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            val controller = ScrollableController(
+                consumeScrollDelta = {
+                    val expected = lastReceivedPreScrollAvailable * (1 - preConsumeFraction)
+                    assertThat(it - expected).isWithin(0.01f)
+                    value += it
+                    it
+                },
+                flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+                animationClock = animationClock
+            )
+            val preConsumingParent = object : NestedScrollConnection {
+                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                    lastReceivedPreScrollAvailable = available.x
+                    return available * preConsumeFraction
+                }
+
+                override fun onPreFling(available: Velocity): Velocity {
+                    // consume all velocity
+                    return available
+                }
+            }
+
+            rule.setContent {
+                Box {
+                    Box(
+                        contentAlignment = Alignment.Center,
+                        modifier = Modifier
+                            .preferredSize(300.dp)
+                            .nestedScroll(preConsumingParent)
+                    ) {
+                        Box(
+                            modifier = Modifier.preferredSize(300.dp)
+                                .testTag(scrollableBoxTag)
+                                .scrollable(
+                                    controller = controller,
+                                    orientation = Orientation.Horizontal
+                                )
+                        )
+                    }
+                }
+            }
+
+            rule.onNodeWithTag(scrollableBoxTag).performGesture {
+                this.swipe(
+                    start = this.center,
+                    end = Offset(this.center.x + 200f, this.center.y),
+                    duration = 300.milliseconds
+                )
+            }
+
+            val preFlingValue = rule.runOnIdle { value }
+            advanceClockWhileAwaitersExist(clock)
+            advanceClockWhileAwaitersExist(clock)
+            rule.runOnIdle {
+                // if scrollable respects prefling consumption, it should fling 0px since we
+                // preconsume all
+                assertThat(preFlingValue).isEqualTo(value)
+            }
+        }
+
+    @Test
+    @OptIn(ExperimentalTesting::class)
+    fun scrollable_nestedScrollAbove_proxiesPostCycles() =
+        runBlockingWithManualClock { clock ->
+            var value = 0f
+            var expectedLeft = 0f
+            val velocityFlung = 5000f
+            val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            val controller = ScrollableController(
+                consumeScrollDelta = {
+                    val toConsume = it * 0.345f
+                    value += toConsume
+                    expectedLeft = it - toConsume
+                    toConsume
+                },
+                flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+                animationClock = animationClock
+            )
+            val parent = object : NestedScrollConnection {
+                override fun onPostScroll(
+                    consumed: Offset,
+                    available: Offset,
+                    source: NestedScrollSource
+                ): Offset {
+                    // we should get in post scroll as much as left in controller callback
+                    assertThat(available.x).isEqualTo(expectedLeft)
+                    return available
+                }
+
+                override fun onPostFling(
+                    consumed: Velocity,
+                    available: Velocity,
+                    onFinished: (Velocity) -> Unit
+                ) {
+                    assertThat(available.pixelsPerSecond)
+                        .isEqualTo(
+                            Offset(x = velocityFlung, y = 0f) - consumed.pixelsPerSecond
+                        )
+                    onFinished.invoke(available)
+                }
+            }
+
+            rule.setContent {
+                Box {
+                    Box(
+                        contentAlignment = Alignment.Center,
+                        modifier = Modifier
+                            .preferredSize(300.dp)
+                            .nestedScroll(parent)
+                    ) {
+                        Box(
+                            modifier = Modifier.preferredSize(300.dp)
+                                .testTag(scrollableBoxTag)
+                                .scrollable(
+                                    controller = controller,
+                                    orientation = Orientation.Horizontal
+                                )
+                        )
+                    }
+                }
+            }
+
+            rule.onNodeWithTag(scrollableBoxTag).performGesture {
+                this.swipeWithVelocity(
+                    start = this.center,
+                    end = Offset(this.center.x + 500f, this.center.y),
+                    duration = 300.milliseconds,
+                    endVelocity = velocityFlung
+                )
+            }
+
+            advanceClockWhileAwaitersExist(clock)
+            advanceClockWhileAwaitersExist(clock)
+
+            // all assertions in callback above
+        }
+
+    @Test
+    @OptIn(ExperimentalTesting::class)
+    fun scrollable_nestedScrollBelow_listensDispatches() =
+        runBlockingWithManualClock { clock ->
+            var value = 0f
+            var expectedConsumed = 0f
+            val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            val controller = ScrollableController(
+                consumeScrollDelta = {
+                    expectedConsumed = it * 0.3f
+                    value += expectedConsumed
+                    expectedConsumed
+                },
+                flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+                animationClock = animationClock
+            )
+            val child = object : NestedScrollConnection {}
+            val dispatcher = NestedScrollDispatcher()
+
+            rule.setContent {
+                Box {
+                    Box(
+                        modifier = Modifier.preferredSize(300.dp)
+
+                            .scrollable(
+                                controller = controller,
+                                orientation = Orientation.Horizontal
+                            )
+                    ) {
+                        Box(
+                            Modifier.preferredSize(200.dp)
+                                .testTag(scrollableBoxTag)
+                                .nestedScroll(child, dispatcher)
+                        )
+                    }
+                }
+            }
+
+            val lastValueBeforeFling = rule.runOnIdle {
+                val preScrollConsumed = dispatcher
+                    .dispatchPreScroll(Offset(20f, 20f), NestedScrollSource.Drag)
+                // scrollable is not interested in pre scroll
+                assertThat(preScrollConsumed).isEqualTo(Offset.Zero)
+
+                val consumed = dispatcher.dispatchPostScroll(
+                    Offset(20f, 20f),
+                    Offset(50f, 50f),
+                    NestedScrollSource.Drag
+                )
+                assertThat(consumed.x - expectedConsumed).isWithin(0.001f)
+
+                val preFlingConsumed = dispatcher
+                    .dispatchPreFling(Velocity(Offset(50f, 50f)))
+                // scrollable won't participate in the pre fling
+                assertThat(preFlingConsumed).isEqualTo(Velocity.Zero)
+
+                dispatcher.dispatchPostFling(
+                    Velocity(Offset(1000f, 1000f)),
+                    Velocity(Offset(2000f, 2000f))
+                )
+                value
+            }
+
+            advanceClockWhileAwaitersExist(clock)
+            advanceClockWhileAwaitersExist(clock)
+
+            rule.runOnIdle {
+                // catch that scrollable caught our post fling and flung
+                assertThat(value).isGreaterThan(lastValueBeforeFling)
+            }
+        }
+
+    @Test
+    @OptIn(ExperimentalTesting::class)
     fun scrollable_interactionState() = runBlocking {
         val interactionState = InteractionState()
         var total = 0f
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
index 847de67..fc5d1c5 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
@@ -27,9 +27,8 @@
 import androidx.compose.testutils.assertPixels
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.RectangleShape
@@ -54,7 +53,7 @@
 import java.util.concurrent.TimeUnit
 
 @LargeTest
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
 class TextFieldCursorTest {
 
     @get:Rule
@@ -83,7 +82,7 @@
                 modifier = Modifier
                     .preferredSize(width, height)
                     .background(Color.White)
-                    .focusObserver { if (it.isFocused) latch.countDown() },
+                    .onFocusChanged { if (it.isFocused) latch.countDown() },
                 cursorColor = Color.Red
             )
         }
@@ -118,7 +117,7 @@
                     modifier = Modifier
                         .preferredSize(width, height)
                         .background(Color.White)
-                        .focusObserver { if (it.isFocused) latch.countDown() },
+                        .onFocusChanged { if (it.isFocused) latch.countDown() },
                     cursorColor = Color.Red
                 )
             }
@@ -168,7 +167,7 @@
                     modifier = Modifier
                         .preferredSize(width, height)
                         .background(Color.White)
-                        .focusObserver { if (it.isFocused) latch.countDown() },
+                        .onFocusChanged { if (it.isFocused) latch.countDown() },
                     cursorColor = Color.Unspecified
                 )
             }
@@ -223,8 +222,7 @@
                     modifier = Modifier
                         .preferredSize(width, height)
                         .background(Color.White)
-                        .focusObserver { if (it.isFocused) latch.countDown() },
-
+                        .onFocusChanged { if (it.isFocused) latch.countDown() },
                     cursorColor = Color.Red
                 )
             }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt
index 53591c6..44e43a4 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt
@@ -22,10 +22,9 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.dp
@@ -37,7 +36,6 @@
 import org.junit.runner.RunWith
 
 @LargeTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class TextFieldFocusTest {
     @get:Rule
@@ -51,7 +49,7 @@
                 value = editor.value,
                 modifier = Modifier
                     .focusRequester(data.focusRequester)
-                    .focusObserver { data.focused = it.isFocused }
+                    .onFocusChanged { data.focused = it.isFocused }
                     .width(10.dp),
                 onValueChange = {
                     editor.value = it
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
index 3afb9ea..0009365 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
@@ -57,6 +57,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
@@ -112,6 +113,7 @@
                         remember { scrollerPosition },
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef
                     )
             )
@@ -142,6 +144,7 @@
                         remember { scrollerPosition },
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef,
                     )
             )
@@ -172,6 +175,7 @@
                         remember { scrollerPosition },
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef,
                     )
             )
@@ -203,6 +207,7 @@
                         remember { scrollerPosition },
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef,
                     )
             )
@@ -231,6 +236,7 @@
                         remember { scrollerPosition },
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef,
                     )
             )
@@ -242,6 +248,7 @@
     }
 
     @Test
+    @LargeTest
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun testTextField_horizontal_scrolledAndClipped() {
         val scrollerPosition = TextFieldScrollerPosition()
@@ -271,6 +278,7 @@
                                 remember { scrollerPosition },
                                 value,
                                 VisualTransformation.None,
+                                remember { InteractionState() },
                                 textLayoutResultRef
                             )
                     )
@@ -288,6 +296,7 @@
     }
 
     @Test
+    @LargeTest
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun testTextField_vertical_scrolledAndClipped() {
         val scrollerPosition = TextFieldScrollerPosition()
@@ -316,6 +325,7 @@
                                 remember { scrollerPosition },
                                 value,
                                 VisualTransformation.None,
+                                remember { InteractionState() },
                                 textLayoutResultRef
                             )
                     )
@@ -354,6 +364,7 @@
                         remember { scrollerPosition },
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef
                     )
             )
@@ -398,6 +409,7 @@
                         remember { scrollerPosition },
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef
                     )
             )
@@ -450,6 +462,7 @@
                         scrollerPosition,
                         value,
                         VisualTransformation.None,
+                        remember { InteractionState() },
                         textLayoutResultRef
                     )
             )
@@ -482,7 +495,12 @@
         val value = TextFieldValue()
         rule.setContent {
             val modifier = Modifier.textFieldScroll(
-                orientation, position, value, VisualTransformation.None, Ref()
+                orientation,
+                position,
+                value,
+                VisualTransformation.None,
+                remember { InteractionState() },
+                Ref()
             ) as InspectableValue
             assertThat(modifier.nameFallback).isEqualTo("textFieldScroll")
             assertThat(modifier.valueOverride).isNull()
@@ -491,6 +509,7 @@
                 "scrollerPosition",
                 "textFieldValue",
                 "visualTransformation",
+                "interactionState",
                 "textLayoutResult"
             )
         }
@@ -532,6 +551,7 @@
                             remember { textFieldScrollPosition },
                             value,
                             VisualTransformation.None,
+                            remember { InteractionState() },
                             textLayoutResultRef
                         ),
                     textStyle = TextStyle(fontSize = 20.sp)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
index 1c589bf..b7e6d80 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
@@ -37,9 +37,8 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.layout.onGloballyPositioned
@@ -97,10 +96,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalFoundationApi::class)
 class TextFieldTest {
     @get:Rule
     val rule = createComposeRule()
@@ -119,7 +115,7 @@
             ) {
                 BasicTextField(
                     value = state.value,
-                    modifier = Modifier.fillMaxSize().focusObserver { isFocused = it.isFocused },
+                    modifier = Modifier.fillMaxSize().onFocusChanged { isFocused = it.isFocused },
                     onValueChange = { state.value = it }
                 )
             }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyArrangementsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyArrangementsTest.kt
new file mode 100644
index 0000000..994dba7
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyArrangementsTest.kt
@@ -0,0 +1,377 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.InternalLayoutApi
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Providers
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(InternalLayoutApi::class)
+class LazyArrangementsTest {
+
+    private val ContainerTag = "ContainerTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+    private var containerSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+        containerSize = itemSize * 5
+    }
+
+    // cases when we have not enough items to fill min constraints:
+
+    @Test
+    fun column_defaultArrangementIsTop() {
+        rule.setContent {
+            LazyColumn(
+                modifier = Modifier.size(containerSize)
+            ) {
+                items((0..1).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Top)
+    }
+
+    @Test
+    fun column_centerArrangement() {
+        composeColumnWith(Arrangement.Center)
+        assertArrangementForTwoItems(Arrangement.Center)
+    }
+
+    @Test
+    fun column_bottomArrangement() {
+        composeColumnWith(Arrangement.Bottom)
+        assertArrangementForTwoItems(Arrangement.Bottom)
+    }
+
+    @Test
+    fun column_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeColumnWith(arrangement)
+        assertArrangementForTwoItems(arrangement)
+    }
+
+    @Test
+    fun row_defaultArrangementIsStart() {
+        rule.setContent {
+            LazyRow(
+                modifier = Modifier.size(containerSize)
+            ) {
+                items((0..1).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_centerArrangement() {
+        composeRowWith(Arrangement.Center, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_endArrangement() {
+        composeRowWith(Arrangement.End, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeRowWith(arrangement, LayoutDirection.Ltr)
+        assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
+    }
+
+    @Test
+    fun row_rtl_startArrangement() {
+        composeRowWith(Arrangement.Center, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
+    }
+
+    @Test
+    fun row_rtl_endArrangement() {
+        composeRowWith(Arrangement.End, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
+    }
+
+    @Test
+    fun row_rtl_spacedArrangementNotFillingViewport() {
+        val arrangement = Arrangement.spacedBy(10.dp)
+        composeRowWith(arrangement, LayoutDirection.Rtl)
+        assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
+    }
+
+    // wrap content and spacing
+
+    @Test
+    fun column_spacing_affects_wrap_content() {
+        rule.setContent {
+            LazyColumn(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.testTag(ContainerTag)
+            ) {
+                items((0..1).toList()) {
+                    Box(Modifier.size(itemSize))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertWidthIsEqualTo(itemSize)
+            .assertHeightIsEqualTo(itemSize * 3)
+    }
+
+    @Test
+    fun row_spacing_affects_wrap_content() {
+        rule.setContent {
+            LazyRow(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.testTag(ContainerTag)
+            ) {
+                items((0..1).toList()) {
+                    Box(Modifier.size(itemSize))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertWidthIsEqualTo(itemSize * 3)
+            .assertHeightIsEqualTo(itemSize)
+    }
+
+    // spacing added when we have enough items to fill the viewport
+
+    @Test
+    fun column_spacing_scrolledToTheTop() {
+        rule.setContent {
+            LazyColumn(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.size(itemSize * 3.5f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun column_spacing_scrolledToTheBottom() {
+        rule.setContent {
+            LazyColumn(
+                verticalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.size(itemSize * 3.5f).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(y = itemSize * 2, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
+    }
+
+    @Test
+    fun row_spacing_scrolledToTheStart() {
+        rule.setContent {
+            LazyRow(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.size(itemSize * 3.5f)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+    }
+
+    @Test
+    fun row_spacing_scrolledToTheEnd() {
+        rule.setContent {
+            LazyRow(
+                horizontalArrangement = Arrangement.spacedBy(itemSize),
+                modifier = Modifier.size(itemSize * 3.5f).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(x = itemSize * 2, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
+
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
+    }
+
+    // with reverseLayout == true
+
+    @Test
+    fun column_defaultArrangementIsBottomWithReverseLayout() {
+        rule.setContent {
+            LazyColumn(
+                reverseLayout = true,
+                modifier = Modifier.size(containerSize)
+            ) {
+                items((0..1).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(Arrangement.Bottom, reversedItemsOrder = true)
+    }
+
+    @Test
+    fun row_defaultArrangementIsEndWithReverseLayout() {
+        rule.setContent {
+            LazyRow(
+                reverseLayout = true,
+                modifier = Modifier.size(containerSize)
+            ) {
+                items((0..1).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        assertArrangementForTwoItems(
+            Arrangement.End, LayoutDirection.Ltr, reversedItemsOrder = true
+        )
+    }
+
+    fun composeColumnWith(arrangement: Arrangement.Vertical) {
+        rule.setContent {
+            LazyColumn(
+                verticalArrangement = arrangement,
+                modifier = Modifier.size(containerSize)
+            ) {
+                items((0..1).toList()) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+    }
+
+    fun composeRowWith(arrangement: Arrangement.Horizontal, layoutDirection: LayoutDirection) {
+        rule.setContent {
+            Providers(AmbientLayoutDirection provides layoutDirection) {
+                LazyRow(
+                    horizontalArrangement = arrangement,
+                    modifier = Modifier.size(containerSize)
+                ) {
+                    items((0..1).toList()) {
+                        Box(Modifier.size(itemSize).testTag(it.toString()))
+                    }
+                }
+            }
+        }
+    }
+
+    fun assertArrangementForTwoItems(
+        arrangement: Arrangement.Vertical,
+        reversedItemsOrder: Boolean = false
+    ) {
+        with(rule.density) {
+            val sizes = IntArray(2) { itemSize.toIntPx() }
+            val outPositions = IntArray(2) { 0 }
+            arrangement.arrange(containerSize.toIntPx(), sizes, this, outPositions)
+
+            outPositions.forEachIndexed { index, position ->
+                val realIndex = if (reversedItemsOrder) if (index == 0) 1 else 0 else index
+                rule.onNodeWithTag("$realIndex")
+                    .assertTopPositionInRootIsEqualTo(position.toDp())
+            }
+        }
+    }
+
+    fun assertArrangementForTwoItems(
+        arrangement: Arrangement.Horizontal,
+        layoutDirection: LayoutDirection,
+        reversedItemsOrder: Boolean = false
+    ) {
+        with(rule.density) {
+            val sizes = IntArray(2) { itemSize.toIntPx() }
+            val outPositions = IntArray(2) { 0 }
+            arrangement.arrange(containerSize.toIntPx(), sizes, layoutDirection, this, outPositions)
+
+            outPositions.forEachIndexed { index, position ->
+                val realIndex = if (reversedItemsOrder) if (index == 0) 1 else 0 else index
+                val expectedPosition = if (layoutDirection == LayoutDirection.Ltr) {
+                    position.toDp()
+                } else {
+                    containerSize - position.toDp() - itemSize
+                }
+                rule.onNodeWithTag("$realIndex")
+                    .assertLeftPositionInRootIsEqualTo(expectedPosition)
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
deleted file mode 100644
index 4d6ba65..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
+++ /dev/null
@@ -1,1072 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy
-
-import androidx.compose.animation.core.ExponentialDecay
-import androidx.compose.animation.core.ManualAnimationClock
-import androidx.compose.foundation.animation.FlingConfig
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.preferredHeight
-import androidx.compose.foundation.layout.preferredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.onCommit
-import androidx.compose.runtime.onDispose
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.TouchSlop
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.center
-import androidx.compose.ui.test.click
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onChildren
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performGesture
-import androidx.compose.ui.test.swipeUp
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.collect.Range
-import com.google.common.truth.IntegerSubject
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyColumnForTest {
-    private val LazyColumnForTag = "TestLazyColumnFor"
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun compositionsAreDisposed_whenNodesAreScrolledOff() {
-        var composed: Boolean
-        var disposed = false
-        // Ten 31dp spacers in a 300dp list
-        val latch = CountDownLatch(10)
-        // Make it long enough that it's _definitely_ taller than the screen
-        val data = (1..50).toList()
-
-        rule.setContent {
-            // Fixed height to eliminate device size as a factor
-            Box(Modifier.testTag(LazyColumnForTag).preferredHeight(300.dp)) {
-                LazyColumnFor(items = data, modifier = Modifier.fillMaxSize()) {
-                    onCommit {
-                        composed = true
-                        // Signal when everything is done composing
-                        latch.countDown()
-                        onDispose {
-                            disposed = true
-                        }
-                    }
-
-                    // There will be 10 of these in the 300dp box
-                    Spacer(Modifier.preferredHeight(31.dp))
-                }
-            }
-        }
-
-        latch.await()
-        composed = false
-
-        assertWithMessage("Compositions were disposed before we did any scrolling")
-            .that(disposed).isFalse()
-
-        // Mostly a validity check, this is not part of the behavior under test
-        assertWithMessage("Additional composition occurred for no apparent reason")
-            .that(composed).isFalse()
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .performGesture { swipeUp() }
-
-        rule.waitForIdle()
-
-        assertWithMessage("No additional items were composed after scroll, scroll didn't work")
-            .that(composed).isTrue()
-
-        // We may need to modify this test once we prefetch/cache items outside the viewport
-        assertWithMessage(
-            "No compositions were disposed after scrolling, compositions were leaked"
-        ).that(disposed).isTrue()
-    }
-
-    @Test
-    fun compositionsAreDisposed_whenDataIsChanged() {
-        var composed = 0
-        var disposals = 0
-        val data1 = (1..3).toList()
-        val data2 = (4..5).toList() // smaller, to ensure removal is handled properly
-
-        var part2 by mutableStateOf(false)
-
-        rule.setContent {
-            LazyColumnFor(
-                items = if (!part2) data1 else data2,
-                modifier = Modifier.testTag(LazyColumnForTag).fillMaxSize()
-            ) {
-                onCommit {
-                    composed++
-                    onDispose {
-                        disposals++
-                    }
-                }
-
-                Spacer(Modifier.height(50.dp))
-            }
-        }
-
-        rule.runOnIdle {
-            assertWithMessage("Not all items were composed")
-                .that(composed).isEqualTo(data1.size)
-            composed = 0
-
-            part2 = true
-        }
-
-        rule.runOnIdle {
-            assertWithMessage(
-                "No additional items were composed after data change, something didn't work"
-            ).that(composed).isEqualTo(data2.size)
-
-            // We may need to modify this test once we prefetch/cache items outside the viewport
-            assertWithMessage(
-                "Not enough compositions were disposed after scrolling, compositions were leaked"
-            ).that(disposals).isEqualTo(data1.size)
-        }
-    }
-
-    @Test
-    fun compositionsAreDisposed_whenAdapterListIsDisposed() {
-        var emitAdapterList by mutableStateOf(true)
-        var disposeCalledOnFirstItem = false
-        var disposeCalledOnSecondItem = false
-
-        rule.setContent {
-            if (emitAdapterList) {
-                LazyColumnFor(
-                    items = listOf(0, 1),
-                    modifier = Modifier.fillMaxSize()
-                ) {
-                    Box(Modifier.size(100.dp))
-                    onDispose {
-                        if (it == 1) {
-                            disposeCalledOnFirstItem = true
-                        } else {
-                            disposeCalledOnSecondItem = true
-                        }
-                    }
-                }
-            }
-        }
-
-        rule.runOnIdle {
-            assertWithMessage("First item is not immediately disposed")
-                .that(disposeCalledOnFirstItem).isFalse()
-            assertWithMessage("Second item is not immediately disposed")
-                .that(disposeCalledOnFirstItem).isFalse()
-            emitAdapterList = false
-        }
-
-        rule.runOnIdle {
-            assertWithMessage("First item is correctly disposed")
-                .that(disposeCalledOnFirstItem).isTrue()
-            assertWithMessage("Second item is correctly disposed")
-                .that(disposeCalledOnSecondItem).isTrue()
-        }
-    }
-
-    @Test
-    fun removeItemsTest() {
-        val startingNumItems = 3
-        var numItems = startingNumItems
-        var numItemsModel by mutableStateOf(numItems)
-        val tag = "List"
-        rule.setContent {
-            LazyColumnFor((1..numItemsModel).toList(), modifier = Modifier.testTag(tag)) {
-                BasicText("$it")
-            }
-        }
-
-        while (numItems >= 0) {
-            // Confirm the number of children to ensure there are no extra items
-            rule.onNodeWithTag(tag)
-                .onChildren()
-                .assertCountEquals(numItems)
-
-            // Confirm the children's content
-            for (i in 1..3) {
-                rule.onNodeWithText("$i").apply {
-                    if (i <= numItems) {
-                        assertExists()
-                    } else {
-                        assertDoesNotExist()
-                    }
-                }
-            }
-            numItems--
-            if (numItems >= 0) {
-                // Don't set the model to -1
-                rule.runOnIdle { numItemsModel = numItems }
-            }
-        }
-    }
-
-    @Test
-    fun changingDataTest() {
-        val dataLists = listOf(
-            (1..3).toList(),
-            (4..8).toList(),
-            (3..4).toList()
-        )
-        var dataModel by mutableStateOf(dataLists[0])
-        val tag = "List"
-        rule.setContent {
-            LazyColumnFor(dataModel, modifier = Modifier.testTag(tag)) {
-                BasicText("$it")
-            }
-        }
-
-        for (data in dataLists) {
-            rule.runOnIdle { dataModel = data }
-
-            // Confirm the number of children to ensure there are no extra items
-            val numItems = data.size
-            rule.onNodeWithTag(tag)
-                .onChildren()
-                .assertCountEquals(numItems)
-
-            // Confirm the children's content
-            for (item in data) {
-                rule.onNodeWithText("$item").assertExists()
-            }
-        }
-    }
-
-    @Test
-    fun whenItemsAreInitiallyCreatedWith0SizeWeCanScrollWhenTheyExpanded() {
-        val thirdTag = "third"
-        val items = (1..3).toList()
-        var thirdHasSize by mutableStateOf(false)
-
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.fillMaxWidth()
-                    .preferredHeight(100.dp)
-                    .testTag(LazyColumnForTag)
-            ) {
-                if (it == 3) {
-                    Spacer(
-                        Modifier.testTag(thirdTag)
-                            .fillParentMaxWidth()
-                            .preferredHeight(if (thirdHasSize) 60.dp else 0.dp)
-                    )
-                } else {
-                    Spacer(Modifier.fillParentMaxWidth().preferredHeight(60.dp))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 21.dp, density = rule.density)
-
-        rule.onNodeWithTag(thirdTag)
-            .assertExists()
-            .assertIsNotDisplayed()
-
-        rule.runOnIdle {
-            thirdHasSize = true
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 10.dp, density = rule.density)
-
-        rule.onNodeWithTag(thirdTag)
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun lazyColumnWrapsContent() = with(rule.density) {
-        val itemInsideLazyColumn = "itemInsideLazyColumn"
-        val itemOutsideLazyColumn = "itemOutsideLazyColumn"
-        var sameSizeItems by mutableStateOf(true)
-
-        rule.setContent {
-            Row {
-                LazyColumnFor(
-                    items = listOf(1, 2),
-                    modifier = Modifier.testTag(LazyColumnForTag)
-                ) {
-                    if (it == 1) {
-                        Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyColumn))
-                    } else {
-                        Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
-                    }
-                }
-                Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyColumn))
-            }
-        }
-
-        rule.onNodeWithTag(itemInsideLazyColumn)
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag(itemOutsideLazyColumn)
-            .assertIsDisplayed()
-
-        var lazyColumnBounds = rule.onNodeWithTag(LazyColumnForTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
-        assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
-
-        rule.runOnIdle {
-            sameSizeItems = false
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(itemInsideLazyColumn)
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag(itemOutsideLazyColumn)
-            .assertIsDisplayed()
-
-        lazyColumnBounds = rule.onNodeWithTag(LazyColumnForTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
-        assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
-    }
-
-    private val firstItemTag = "firstItemTag"
-    private val secondItemTag = "secondItemTag"
-
-    private fun prepareLazyColumnsItemsAlignment(horizontalGravity: Alignment.Horizontal) {
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(1, 2),
-                modifier = Modifier.testTag(LazyColumnForTag).width(100.dp),
-                horizontalAlignment = horizontalGravity
-            ) {
-                if (it == 1) {
-                    Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
-                } else {
-                    Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertIsDisplayed()
-
-        val lazyColumnBounds = rule.onNodeWithTag(LazyColumnForTag)
-            .getUnclippedBoundsInRoot()
-
-        with(rule.density) {
-            // Verify the width of the column
-            assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-            assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
-        }
-    }
-
-    @Test
-    fun lazyColumnAlignmentCenterHorizontally() {
-        prepareLazyColumnsItemsAlignment(Alignment.CenterHorizontally)
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertPositionInRootIsEqualTo(25.dp, 0.dp)
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertPositionInRootIsEqualTo(15.dp, 50.dp)
-    }
-
-    @Test
-    fun lazyColumnAlignmentStart() {
-        prepareLazyColumnsItemsAlignment(Alignment.Start)
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertPositionInRootIsEqualTo(0.dp, 50.dp)
-    }
-
-    @Test
-    fun lazyColumnAlignmentEnd() {
-        prepareLazyColumnsItemsAlignment(Alignment.End)
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertPositionInRootIsEqualTo(50.dp, 0.dp)
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertPositionInRootIsEqualTo(30.dp, 50.dp)
-    }
-
-    @Test
-    fun itemFillingParentWidth() {
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(100.dp)
-            .assertHeightIsEqualTo(50.dp)
-    }
-
-    @Test
-    fun itemFillingParentHeight() {
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(50.dp)
-            .assertHeightIsEqualTo(150.dp)
-    }
-
-    @Test
-    fun itemFillingParentSize() {
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(100.dp)
-            .assertHeightIsEqualTo(150.dp)
-    }
-
-    @Test
-    fun itemFillingParentWidthFraction() {
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxWidth(0.6f).height(50.dp).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(60.dp)
-            .assertHeightIsEqualTo(50.dp)
-    }
-
-    @Test
-    fun itemFillingParentHeightFraction() {
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.2f).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(50.dp)
-            .assertHeightIsEqualTo(30.dp)
-    }
-
-    @Test
-    fun itemFillingParentSizeFraction() {
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxSize(0.1f).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(10.dp)
-            .assertHeightIsEqualTo(15.dp)
-    }
-
-    @Test
-    fun itemFillingParentSizeParentResized() {
-        var parentSize by mutableStateOf(100.dp)
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(0),
-                modifier = Modifier.size(parentSize)
-            ) {
-                Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
-            }
-        }
-
-        rule.runOnIdle {
-            parentSize = 150.dp
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(150.dp)
-            .assertHeightIsEqualTo(150.dp)
-    }
-
-    @Test
-    fun whenNotAnymoreAvailableItemWasDisplayed() {
-        var items by mutableStateOf((1..30).toList())
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 16-20
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 300.dp, density = rule.density)
-
-        rule.runOnIdle {
-            items = (1..10).toList()
-        }
-
-        // there is no item 16 anymore so we will just display the last items 6-10
-        rule.onNodeWithTag("6")
-            .assertTopPositionIsAlmost(0.dp)
-    }
-
-    @Test
-    fun whenFewDisplayedItemsWereRemoved() {
-        var items by mutableStateOf((1..10).toList())
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 6-10
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 100.dp, density = rule.density)
-
-        rule.runOnIdle {
-            items = (1..8).toList()
-        }
-
-        // there are no more items 9 and 10, so we have to scroll back
-        rule.onNodeWithTag("4")
-            .assertTopPositionIsAlmost(0.dp)
-    }
-
-    @Test
-    fun whenItemsBecameEmpty() {
-        var items by mutableStateOf((1..10).toList())
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 2-6
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 20.dp, density = rule.density)
-
-        rule.runOnIdle {
-            items = emptyList()
-        }
-
-        // there are no more items so the LazyColumn is zero sized
-        rule.onNodeWithTag(LazyColumnForTag)
-            .assertWidthIsEqualTo(0.dp)
-            .assertHeightIsEqualTo(0.dp)
-
-        // and has no children
-        rule.onNodeWithTag("1")
-            .assertDoesNotExist()
-        rule.onNodeWithTag("2")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun scrollBackAndForth() {
-        val items by mutableStateOf((1..20).toList())
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 6-10
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 100.dp, density = rule.density)
-
-        // and scroll back
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = (-100).dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertTopPositionIsAlmost(0.dp)
-    }
-
-    @Test
-    fun tryToScrollBackwardWhenAlreadyOnTop() {
-        val items by mutableStateOf((1..20).toList())
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // we already displaying the first item, so this should do nothing
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = (-50).dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertTopPositionIsAlmost(0.dp)
-        rule.onNodeWithTag("5")
-            .assertTopPositionIsAlmost(80.dp)
-    }
-
-    @Test
-    fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
-        val items = listOf(NotStable(1), NotStable(2))
-        var firstItemRecomposed = 0
-        var secondItemRecomposed = 0
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                if (it.count == 1) {
-                    firstItemRecomposed++
-                } else {
-                    secondItemRecomposed++
-                }
-                Spacer(Modifier.size(75.dp))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(firstItemRecomposed).isEqualTo(1)
-            assertThat(secondItemRecomposed).isEqualTo(1)
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = (50).dp, density = rule.density)
-
-        rule.runOnIdle {
-            assertThat(firstItemRecomposed).isEqualTo(1)
-            assertThat(secondItemRecomposed).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun onlyOneMeasurePassForScrollEvent() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        val initialMeasurePasses = state.numMeasurePasses
-
-        rule.runOnIdle {
-            with(rule.density) {
-                state.onScroll(-110.dp.toPx())
-            }
-        }
-
-        rule.waitForIdle()
-
-        assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
-    }
-
-    @Test
-    fun stateUpdatedAfterScroll() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 30.dp, density = rule.density)
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
-
-            with(rule.density) {
-                // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
-                //  number of pixels
-                val expectedOffset = 10.dp.toIntPx()
-                val tolerance = 2.dp.toIntPx()
-                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
-            }
-        }
-    }
-
-    @Test
-    fun isAnimationRunningUpdate() {
-        val items by mutableStateOf((1..20).toList())
-        val clock = ManualAnimationClock(0L)
-        val state = LazyListState(
-            flingConfig = FlingConfig(ExponentialDecay()),
-            animationClock = clock
-        )
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-            assertThat(state.isAnimationRunning).isEqualTo(false)
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .performGesture { swipeUp() }
-
-        rule.runOnIdle {
-            clock.clockTimeMillis += 100
-            assertThat(state.firstVisibleItemIndex).isNotEqualTo(0)
-            assertThat(state.isAnimationRunning).isEqualTo(true)
-        }
-
-        // TODO (jelle): this should be down, and not click to be 100% fair
-        rule.onNodeWithTag(LazyColumnForTag)
-            .performGesture { click() }
-
-        rule.runOnIdle {
-            assertThat(state.isAnimationRunning).isEqualTo(false)
-        }
-    }
-
-    @Test
-    fun stateUpdatedAfterScrollWithinTheSameItem() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 10.dp, density = rule.density)
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-            with(rule.density) {
-                val expectedOffset = 10.dp.toIntPx()
-                val tolerance = 2.dp.toIntPx()
-                assertThat(state.firstVisibleItemScrollOffset)
-                    .isEqualTo(expectedOffset, tolerance)
-            }
-        }
-    }
-
-    @Test
-    fun initialScrollIsApplied() {
-        val items by mutableStateOf((0..20).toList())
-        lateinit var state: LazyListState
-        val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
-        rule.setContent {
-            state = rememberLazyListState(2, expectedOffset)
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(2)
-            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
-        }
-
-        rule.onNodeWithTag("2")
-            .assertTopPositionInRootIsEqualTo((-10).dp)
-    }
-
-    @Test
-    fun stateIsRestored() {
-        val restorationTester = StateRestorationTester(rule)
-        val items by mutableStateOf((1..20).toList())
-        var state: LazyListState? = null
-        restorationTester.setContent {
-            state = rememberLazyListState()
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
-                state = state!!
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 30.dp, density = rule.density)
-
-        val (index, scrollOffset) = rule.runOnIdle {
-            state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
-        }
-
-        state = null
-
-        restorationTester.emulateSavedInstanceStateRestore()
-
-        rule.runOnIdle {
-            assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
-            assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
-        }
-    }
-
-    @Test
-    fun scroll_makeListSmaller_scroll() {
-        var items by mutableStateOf((1..100).toList())
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(Modifier.size(10.dp).testTag("$it"))
-            }
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 300.dp, density = rule.density)
-
-        rule.runOnIdle {
-            items = (1..11).toList()
-        }
-
-        // try to scroll after the data set has been updated. this was causing a crash previously
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = (-10).dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun snapToItemIndex() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.runOnIdle {
-            runBlocking {
-                state.snapToItemIndex(3, 10)
-            }
-            assertThat(state.firstVisibleItemIndex).isEqualTo(3)
-            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
-        }
-    }
-
-    @Test
-    fun itemsAreNotRedrawnDuringScroll() {
-        val items = (0..20).toList()
-        val redrawCount = Array(6) { 0 }
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(
-                    Modifier.size(20.dp)
-                        .drawBehind { redrawCount[it]++ }
-                )
-            }
-        }
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 10.dp, density = rule.density)
-
-        rule.runOnIdle {
-            redrawCount.forEachIndexed { index, i ->
-                assertWithMessage("Item with index $index was redrawn $i times")
-                    .that(i).isEqualTo(1)
-            }
-        }
-    }
-
-    @Test
-    fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
-        val items = (0..1).toList()
-        val redrawCount = Array(2) { 0 }
-        var stateUsedInDrawScope by mutableStateOf(false)
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
-            ) {
-                Spacer(
-                    Modifier.size(50.dp)
-                        .drawBehind {
-                            redrawCount[it]++
-                            if (it == 1) {
-                                stateUsedInDrawScope.hashCode()
-                            }
-                        }
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            stateUsedInDrawScope = true
-        }
-
-        rule.runOnIdle {
-            assertWithMessage("First items is not expected to be redrawn")
-                .that(redrawCount[0]).isEqualTo(1)
-            assertWithMessage("Second items is expected to be redrawn")
-                .that(redrawCount[1]).isEqualTo(2)
-        }
-    }
-
-    private fun SemanticsNodeInteraction.assertTopPositionIsAlmost(expected: Dp) {
-        getUnclippedBoundsInRoot().top.assertIsEqualTo(expected, tolerance = 1.dp)
-    }
-}
-
-data class NotStable(val count: Int)
-
-internal fun IntegerSubject.isWithin1PixelFrom(expected: Int) {
-    isEqualTo(expected, 1)
-}
-
-internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
-    isIn(Range.closed(expected - tolerance, expected + tolerance))
-}
-
-internal fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
-    performGesture {
-        with(density) {
-            val touchSlop = TouchSlop.toIntPx()
-            val xPx = x.toIntPx()
-            val yPx = y.toIntPx()
-            val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
-            val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
-            swipeWithVelocity(
-                start = center,
-                end = Offset(center.x - offsetX, center.y - offsetY),
-                endVelocity = 0f
-            )
-        }
-    }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
index 7e629eb3..10e9e7a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
@@ -16,101 +16,78 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.animation.core.ExponentialDecay
+import androidx.compose.animation.core.ManualAnimationClock
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.animation.FlingConfig
+import androidx.compose.foundation.animation.smoothScrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.onCommit
+import androidx.compose.runtime.onDispose
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.TouchSlop
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.center
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performGesture
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
+import androidx.test.filters.LargeTest
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
 
-@MediumTest
+@LargeTest
 @RunWith(AndroidJUnit4::class)
 class LazyColumnTest {
-    private val LazyColumnTag = "LazyColumnTag"
+    private val LazyListTag = "LazyListTag"
 
     @get:Rule
     val rule = createComposeRule()
 
     @Test
-    fun lazyColumnShowsItem() {
-        val itemTestTag = "itemTestTag"
-
-        rule.setContent {
-            LazyColumn {
-                item {
-                    Spacer(
-                        Modifier.preferredHeight(10.dp).fillParentMaxWidth().testTag(itemTestTag)
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(itemTestTag)
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun lazyColumnShowsItems() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyColumn(Modifier.preferredHeight(200.dp)) {
-                items(items) {
-                    Spacer(Modifier.preferredHeight(101.dp).fillParentMaxWidth().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag("1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("4")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun lazyColumnShowsIndexedItems() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyColumn(Modifier.preferredHeight(200.dp)) {
-                itemsIndexed(items) { index, item ->
-                    Spacer(
-                        Modifier.preferredHeight(101.dp).fillParentMaxWidth()
-                            .testTag("$index-$item")
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag("0-1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("1-2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2-3")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("3-4")
-            .assertDoesNotExist()
-    }
-
-    @Test
     fun lazyColumnShowsCombinedItems() {
         val itemTestTag = "itemTestTag"
         val items = listOf(1, 2).map { it.toString() }
@@ -155,59 +132,6 @@
     }
 
     @Test
-    fun lazyColumnShowsItemsOnScroll() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyColumn(Modifier.preferredHeight(200.dp).testTag(LazyColumnTag)) {
-                items(items) {
-                    Spacer(Modifier.preferredHeight(101.dp).fillParentMaxWidth().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyColumnTag)
-            .scrollBy(y = 50.dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("4")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun lazyColumnScrollHidesItem() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyColumn(Modifier.preferredHeight(200.dp).testTag(LazyColumnTag)) {
-                items(items) {
-                    Spacer(Modifier.preferredHeight(101.dp).fillParentMaxWidth().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyColumnTag)
-            .scrollBy(y = 103.dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertIsDisplayed()
-    }
-
-    @Test
     fun lazyColumnAllowEmptyListItems() {
         val itemTag = "itemTag"
 
@@ -253,4 +177,1050 @@
         rule.onNodeWithTag("3")
             .assertDoesNotExist()
     }
-}
\ No newline at end of file
+
+    @Test
+    fun compositionsAreDisposed_whenNodesAreScrolledOff() {
+        var composed: Boolean
+        var disposed = false
+        // Ten 31dp spacers in a 300dp list
+        val latch = CountDownLatch(10)
+        // Make it long enough that it's _definitely_ taller than the screen
+        val data = (1..50).toList()
+
+        rule.setContent {
+            // Fixed height to eliminate device size as a factor
+            Box(Modifier.testTag(LazyListTag).preferredHeight(300.dp)) {
+                LazyColumn(Modifier.fillMaxSize()) {
+                    items(data) {
+                        onCommit {
+                            composed = true
+                            // Signal when everything is done composing
+                            latch.countDown()
+                            onDispose {
+                                disposed = true
+                            }
+                        }
+
+                        // There will be 10 of these in the 300dp box
+                        Spacer(Modifier.preferredHeight(31.dp))
+                    }
+                }
+            }
+        }
+
+        latch.await()
+        composed = false
+
+        assertWithMessage("Compositions were disposed before we did any scrolling")
+            .that(disposed).isFalse()
+
+        // Mostly a validity check, this is not part of the behavior under test
+        assertWithMessage("Additional composition occurred for no apparent reason")
+            .that(composed).isFalse()
+
+        rule.onNodeWithTag(LazyListTag)
+            .performGesture { swipeUp() }
+
+        rule.waitForIdle()
+
+        assertWithMessage("No additional items were composed after scroll, scroll didn't work")
+            .that(composed).isTrue()
+
+        // We may need to modify this test once we prefetch/cache items outside the viewport
+        assertWithMessage(
+            "No compositions were disposed after scrolling, compositions were leaked"
+        ).that(disposed).isTrue()
+    }
+
+    @Test
+    fun compositionsAreDisposed_whenDataIsChanged() {
+        var composed = 0
+        var disposals = 0
+        val data1 = (1..3).toList()
+        val data2 = (4..5).toList() // smaller, to ensure removal is handled properly
+
+        var part2 by mutableStateOf(false)
+
+        rule.setContent {
+            LazyColumn(Modifier.testTag(LazyListTag).fillMaxSize()) {
+                items(if (!part2) data1 else data2) {
+                    onCommit {
+                        composed++
+                        onDispose {
+                            disposals++
+                        }
+                    }
+
+                    Spacer(Modifier.height(50.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("Not all items were composed")
+                .that(composed).isEqualTo(data1.size)
+            composed = 0
+
+            part2 = true
+        }
+
+        rule.runOnIdle {
+            assertWithMessage(
+                "No additional items were composed after data change, something didn't work"
+            ).that(composed).isEqualTo(data2.size)
+
+            // We may need to modify this test once we prefetch/cache items outside the viewport
+            assertWithMessage(
+                "Not enough compositions were disposed after scrolling, compositions were leaked"
+            ).that(disposals).isEqualTo(data1.size)
+        }
+    }
+
+    @Test
+    fun compositionsAreDisposed_whenAdapterListIsDisposed() {
+        var emitAdapterList by mutableStateOf(true)
+        var disposeCalledOnFirstItem = false
+        var disposeCalledOnSecondItem = false
+
+        rule.setContent {
+            if (emitAdapterList) {
+                LazyColumn(Modifier.fillMaxSize()) {
+                    items(listOf(0, 1)) {
+                        Box(Modifier.size(100.dp))
+                        onDispose {
+                            if (it == 1) {
+                                disposeCalledOnFirstItem = true
+                            } else {
+                                disposeCalledOnSecondItem = true
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("First item is not immediately disposed")
+                .that(disposeCalledOnFirstItem).isFalse()
+            assertWithMessage("Second item is not immediately disposed")
+                .that(disposeCalledOnFirstItem).isFalse()
+            emitAdapterList = false
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("First item is correctly disposed")
+                .that(disposeCalledOnFirstItem).isTrue()
+            assertWithMessage("Second item is correctly disposed")
+                .that(disposeCalledOnSecondItem).isTrue()
+        }
+    }
+
+    @Test
+    fun removeItemsTest() {
+        val startingNumItems = 3
+        var numItems = startingNumItems
+        var numItemsModel by mutableStateOf(numItems)
+        val tag = "List"
+        rule.setContent {
+            LazyColumn(Modifier.testTag(tag)) {
+                items((1..numItemsModel).toList()) {
+                    BasicText("$it")
+                }
+            }
+        }
+
+        while (numItems >= 0) {
+            // Confirm the number of children to ensure there are no extra items
+            rule.onNodeWithTag(tag)
+                .onChildren()
+                .assertCountEquals(numItems)
+
+            // Confirm the children's content
+            for (i in 1..3) {
+                rule.onNodeWithText("$i").apply {
+                    if (i <= numItems) {
+                        assertExists()
+                    } else {
+                        assertDoesNotExist()
+                    }
+                }
+            }
+            numItems--
+            if (numItems >= 0) {
+                // Don't set the model to -1
+                rule.runOnIdle { numItemsModel = numItems }
+            }
+        }
+    }
+
+    @Test
+    fun changingDataTest() {
+        val dataLists = listOf(
+            (1..3).toList(),
+            (4..8).toList(),
+            (3..4).toList()
+        )
+        var dataModel by mutableStateOf(dataLists[0])
+        val tag = "List"
+        rule.setContent {
+            LazyColumn(Modifier.testTag(tag)) {
+                items(dataModel) {
+                    BasicText("$it")
+                }
+            }
+        }
+
+        for (data in dataLists) {
+            rule.runOnIdle { dataModel = data }
+
+            // Confirm the number of children to ensure there are no extra items
+            val numItems = data.size
+            rule.onNodeWithTag(tag)
+                .onChildren()
+                .assertCountEquals(numItems)
+
+            // Confirm the children's content
+            for (item in data) {
+                rule.onNodeWithText("$item").assertExists()
+            }
+        }
+    }
+
+    @Test
+    fun whenItemsAreInitiallyCreatedWith0SizeWeCanScrollWhenTheyExpanded() {
+        val thirdTag = "third"
+        val items = (1..3).toList()
+        var thirdHasSize by mutableStateOf(false)
+
+        rule.setContent {
+            LazyColumn(
+                Modifier.fillMaxWidth()
+                    .preferredHeight(100.dp)
+                    .testTag(LazyListTag)
+            ) {
+                items(items) {
+                    if (it == 3) {
+                        Spacer(
+                            Modifier.testTag(thirdTag)
+                                .fillParentMaxWidth()
+                                .preferredHeight(if (thirdHasSize) 60.dp else 0.dp)
+                        )
+                    } else {
+                        Spacer(Modifier.fillParentMaxWidth().preferredHeight(60.dp))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 21.dp, density = rule.density)
+
+        rule.onNodeWithTag(thirdTag)
+            .assertExists()
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            thirdHasSize = true
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 10.dp, density = rule.density)
+
+        rule.onNodeWithTag(thirdTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun lazyColumnWrapsContent() = with(rule.density) {
+        val itemInsideLazyColumn = "itemInsideLazyColumn"
+        val itemOutsideLazyColumn = "itemOutsideLazyColumn"
+        var sameSizeItems by mutableStateOf(true)
+
+        rule.setContent {
+            Row {
+                LazyColumn(Modifier.testTag(LazyListTag)) {
+                    items(listOf(1, 2)) {
+                        if (it == 1) {
+                            Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyColumn))
+                        } else {
+                            Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
+                        }
+                    }
+                }
+                Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyColumn))
+            }
+        }
+
+        rule.onNodeWithTag(itemInsideLazyColumn)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(itemOutsideLazyColumn)
+            .assertIsDisplayed()
+
+        var lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
+        assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+
+        rule.runOnIdle {
+            sameSizeItems = false
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(itemInsideLazyColumn)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(itemOutsideLazyColumn)
+            .assertIsDisplayed()
+
+        lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
+        assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
+    }
+
+    private val firstItemTag = "firstItemTag"
+    private val secondItemTag = "secondItemTag"
+
+    private fun prepareLazyColumnsItemsAlignment(horizontalGravity: Alignment.Horizontal) {
+        rule.setContent {
+            LazyColumn(
+                Modifier.testTag(LazyListTag).width(100.dp),
+                horizontalAlignment = horizontalGravity
+            ) {
+                items(listOf(1, 2)) {
+                    if (it == 1) {
+                        Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
+                    } else {
+                        Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertIsDisplayed()
+
+        val lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        with(rule.density) {
+            // Verify the width of the column
+            assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+            assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+        }
+    }
+
+    @Test
+    fun lazyColumnAlignmentCenterHorizontally() {
+        prepareLazyColumnsItemsAlignment(Alignment.CenterHorizontally)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(25.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(15.dp, 50.dp)
+    }
+
+    @Test
+    fun lazyColumnAlignmentStart() {
+        prepareLazyColumnsItemsAlignment(Alignment.Start)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+    }
+
+    @Test
+    fun lazyColumnAlignmentEnd() {
+        prepareLazyColumnsItemsAlignment(Alignment.End)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(30.dp, 50.dp)
+    }
+
+    @Test
+    fun itemFillingParentWidth() {
+        rule.setContent {
+            LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeight() {
+        rule.setContent {
+            LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentSize() {
+        rule.setContent {
+            LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentWidthFraction() {
+        rule.setContent {
+            LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxWidth(0.6f).height(50.dp).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(60.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeightFraction() {
+        rule.setContent {
+            LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.2f).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(30.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeFraction() {
+        rule.setContent {
+            LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize(0.1f).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(10.dp)
+            .assertHeightIsEqualTo(15.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeParentResized() {
+        var parentSize by mutableStateOf(100.dp)
+        rule.setContent {
+            LazyColumn(Modifier.size(parentSize)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            parentSize = 150.dp
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(150.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun whenNotAnymoreAvailableItemWasDisplayed() {
+        var items by mutableStateOf((1..30).toList())
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 16-20
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 300.dp, density = rule.density)
+
+        rule.runOnIdle {
+            items = (1..10).toList()
+        }
+
+        // there is no item 16 anymore so we will just display the last items 6-10
+        rule.onNodeWithTag("6")
+            .assertTopPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun whenFewDisplayedItemsWereRemoved() {
+        var items by mutableStateOf((1..10).toList())
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 6-10
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 100.dp, density = rule.density)
+
+        rule.runOnIdle {
+            items = (1..8).toList()
+        }
+
+        // there are no more items 9 and 10, so we have to scroll back
+        rule.onNodeWithTag("4")
+            .assertTopPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun whenItemsBecameEmpty() {
+        var items by mutableStateOf((1..10).toList())
+        rule.setContent {
+            LazyColumn(Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 2-6
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 20.dp, density = rule.density)
+
+        rule.runOnIdle {
+            items = emptyList()
+        }
+
+        // there are no more items so the LazyColumn is zero sized
+        rule.onNodeWithTag(LazyListTag)
+            .assertWidthIsEqualTo(0.dp)
+            .assertHeightIsEqualTo(0.dp)
+
+        // and has no children
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun scrollBackAndForth() {
+        val items by mutableStateOf((1..20).toList())
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 6-10
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 100.dp, density = rule.density)
+
+        // and scroll back
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = (-100).dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun tryToScrollBackwardWhenAlreadyOnTop() {
+        val items by mutableStateOf((1..20).toList())
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // we already displaying the first item, so this should do nothing
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = (-50).dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionIsAlmost(0.dp)
+        rule.onNodeWithTag("5")
+            .assertTopPositionIsAlmost(80.dp)
+    }
+
+    @Test
+    fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
+        val items = listOf(NotStable(1), NotStable(2))
+        var firstItemRecomposed = 0
+        var secondItemRecomposed = 0
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    if (it.count == 1) {
+                        firstItemRecomposed++
+                    } else {
+                        secondItemRecomposed++
+                    }
+                    Spacer(Modifier.size(75.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(firstItemRecomposed).isEqualTo(1)
+            assertThat(secondItemRecomposed).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = (50).dp, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(firstItemRecomposed).isEqualTo(1)
+            assertThat(secondItemRecomposed).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun onlyOneMeasurePassForScrollEvent() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumn(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        val initialMeasurePasses = state.numMeasurePasses
+
+        rule.runOnIdle {
+            with(rule.density) {
+                state.onScroll(-110.dp.toPx())
+            }
+        }
+
+        rule.waitForIdle()
+
+        assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
+    }
+
+    @Test
+    fun stateUpdatedAfterScroll() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumn(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 30.dp, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+
+            with(rule.density) {
+                // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
+                //  number of pixels
+                val expectedOffset = 10.dp.toIntPx()
+                val tolerance = 2.dp.toIntPx()
+                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
+            }
+        }
+    }
+
+    @Test
+    fun isAnimationRunningUpdate() {
+        val items by mutableStateOf((1..20).toList())
+        val clock = ManualAnimationClock(0L)
+        val state = LazyListState(
+            flingConfig = FlingConfig(ExponentialDecay()),
+            animationClock = clock
+        )
+        rule.setContent {
+            LazyColumn(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.isAnimationRunning).isEqualTo(false)
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .performGesture { swipeUp() }
+
+        rule.runOnIdle {
+            clock.clockTimeMillis += 100
+            assertThat(state.firstVisibleItemIndex).isNotEqualTo(0)
+            assertThat(state.isAnimationRunning).isEqualTo(true)
+        }
+
+        // TODO (jelle): this should be down, and not click to be 100% fair
+        rule.onNodeWithTag(LazyListTag)
+            .performGesture { click() }
+
+        rule.runOnIdle {
+            assertThat(state.isAnimationRunning).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun stateUpdatedAfterScrollWithinTheSameItem() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumn(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 10.dp, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) {
+                val expectedOffset = 10.dp.toIntPx()
+                val tolerance = 2.dp.toIntPx()
+                assertThat(state.firstVisibleItemScrollOffset)
+                    .isEqualTo(expectedOffset, tolerance)
+            }
+        }
+    }
+
+    @Test
+    fun initialScrollIsApplied() {
+        val items by mutableStateOf((0..20).toList())
+        lateinit var state: LazyListState
+        val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
+        rule.setContent {
+            state = rememberLazyListState(2, expectedOffset)
+            LazyColumn(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo((-10).dp)
+    }
+
+    @Test
+    fun stateIsRestored() {
+        val restorationTester = StateRestorationTester(rule)
+        val items by mutableStateOf((1..20).toList())
+        var state: LazyListState? = null
+        restorationTester.setContent {
+            state = rememberLazyListState()
+            LazyColumn(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state!!
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 30.dp, density = rule.density)
+
+        val (index, scrollOffset) = rule.runOnIdle {
+            state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
+        }
+
+        state = null
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
+            assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
+        }
+    }
+
+    @Test
+    fun scroll_makeListSmaller_scroll() {
+        var items by mutableStateOf((1..100).toList())
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(10.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 300.dp, density = rule.density)
+
+        rule.runOnIdle {
+            items = (1..11).toList()
+        }
+
+        // try to scroll after the data set has been updated. this was causing a crash previously
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = (-10).dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun snapToItemIndex() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyColumn(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.snapToItemIndex(3, 10)
+            }
+            assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+        }
+    }
+
+    @Test
+    fun itemsAreNotRedrawnDuringScroll() {
+        val items = (0..20).toList()
+        val redrawCount = Array(6) { 0 }
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(
+                        Modifier.size(20.dp)
+                            .drawBehind { redrawCount[it]++ }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(y = 10.dp, density = rule.density)
+
+        rule.runOnIdle {
+            redrawCount.forEachIndexed { index, i ->
+                assertWithMessage("Item with index $index was redrawn $i times")
+                    .that(i).isEqualTo(1)
+            }
+        }
+    }
+
+    @Test
+    fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+        val items = (0..1).toList()
+        val redrawCount = Array(2) { 0 }
+        var stateUsedInDrawScope by mutableStateOf(false)
+        rule.setContent {
+            LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(
+                        Modifier.size(50.dp)
+                            .drawBehind {
+                                redrawCount[it]++
+                                if (it == 1) {
+                                    stateUsedInDrawScope.hashCode()
+                                }
+                            }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            stateUsedInDrawScope = true
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("First items is not expected to be redrawn")
+                .that(redrawCount[0]).isEqualTo(1)
+            assertWithMessage("Second items is expected to be redrawn")
+                .that(redrawCount[1]).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+        val items = (0..1).toList()
+        val itemSize = with(rule.density) { 30.toDp() }
+        val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                Modifier.height(itemSizeMinusOne).testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items(items) {
+                    Spacer(
+                        if (it == 0) {
+                            Modifier.width(30.dp).height(itemSizeMinusOne)
+                        } else {
+                            Modifier.width(20.dp).height(itemSize)
+                        }
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag(LazyListTag)
+            .assertWidthIsEqualTo(20.dp)
+    }
+
+    @Test
+    fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+        val items = (0..2).toList()
+        val itemSize = with(rule.density) { 30.toDp() }
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                Modifier.height(itemSize * 1.75f).testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items(items) {
+                    Spacer(
+                        if (it == 0) {
+                            Modifier.width(30.dp).height(itemSize / 2)
+                        } else if (it == 1) {
+                            Modifier.width(20.dp).height(itemSize / 2)
+                        } else {
+                            Modifier.width(20.dp).height(itemSize)
+                        }
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag(LazyListTag)
+            .assertWidthIsEqualTo(30.dp)
+    }
+
+    private fun SemanticsNodeInteraction.assertTopPositionIsAlmost(expected: Dp) {
+        getUnclippedBoundsInRoot().top.assertIsEqualTo(expected, tolerance = 1.dp)
+    }
+
+    private fun LazyListState.scrollBy(offset: Dp) {
+        runBlocking {
+            smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+        }
+    }
+}
+
+data class NotStable(val count: Int)
+
+internal fun IntegerSubject.isWithin1PixelFrom(expected: Int) {
+    isEqualTo(expected, 1)
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+    isIn(Range.closed(expected - tolerance, expected + tolerance))
+}
+
+internal fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
+    performGesture {
+        with(density) {
+            val touchSlop = TouchSlop.toIntPx()
+            val xPx = x.toIntPx()
+            val yPx = y.toIntPx()
+            val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
+            val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
+            swipeWithVelocity(
+                start = center,
+                end = Offset(center.x - offsetX, center.y - offsetY),
+                endVelocity = 0f
+            )
+        }
+    }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyForIndexedTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyForIndexedTest.kt
deleted file mode 100644
index 37c72b8..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyForIndexedTest.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy
-
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.preferredHeight
-import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.dp
-import org.junit.Rule
-import org.junit.Test
-
-class LazyForIndexedTest {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun columnWithIndexesComposedWithCorrectIndexAndItem() {
-        val items = (0..1).map { it.toString() }
-
-        rule.setContent {
-            LazyColumnForIndexed(items, Modifier.preferredHeight(200.dp)) { index, item ->
-                BasicText("${index}x$item", Modifier.fillParentMaxWidth().height(100.dp))
-            }
-        }
-
-        rule.onNodeWithText("0x0")
-            .assertTopPositionInRootIsEqualTo(0.dp)
-
-        rule.onNodeWithText("1x1")
-            .assertTopPositionInRootIsEqualTo(100.dp)
-    }
-
-    @Test
-    fun rowWithIndexesComposedWithCorrectIndexAndItem() {
-        val items = (0..1).map { it.toString() }
-
-        rule.setContent {
-            LazyRowForIndexed(items, Modifier.preferredWidth(200.dp)) { index, item ->
-                BasicText("${index}x$item", Modifier.fillParentMaxHeight().width(100.dp))
-            }
-        }
-
-        rule.onNodeWithText("0x0")
-            .assertLeftPositionInRootIsEqualTo(0.dp)
-
-        rule.onNodeWithText("1x1")
-            .assertLeftPositionInRootIsEqualTo(100.dp)
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
index c3b1ac8..af17e7f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.layout.preferredSize
@@ -23,6 +24,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.dp
@@ -32,6 +34,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalFoundationApi::class)
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class LazyGridTest {
@@ -45,8 +48,8 @@
         val itemTestTag = "itemTestTag"
 
         rule.setContent {
-            LazyGrid(
-                columns = 3
+            LazyVerticalGrid(
+                cells = GridCells.Fixed(3)
             ) {
                 item {
                     Spacer(
@@ -65,8 +68,8 @@
         val items = (1..5).map { it.toString() }
 
         rule.setContent {
-            LazyGrid(
-                columns = 3,
+            LazyVerticalGrid(
+                cells = GridCells.Fixed(3),
                 modifier = Modifier.preferredHeight(100.dp).preferredWidth(300.dp)
             ) {
                 items(items) {
@@ -96,8 +99,8 @@
         val items = (1..9).map { it.toString() }
 
         rule.setContent {
-            LazyGrid(
-                columns = 3,
+            LazyVerticalGrid(
+                cells = GridCells.Fixed(3),
                 modifier = Modifier.preferredHeight(100.dp).testTag(LazyGridTag)
             ) {
                 items(items) {
@@ -133,8 +136,8 @@
         val items = (1..9).map { it.toString() }
 
         rule.setContent {
-            LazyGrid(
-                columns = 3,
+            LazyVerticalGrid(
+                cells = GridCells.Fixed(3),
                 modifier = Modifier.preferredHeight(200.dp).testTag(LazyGridTag)
             ) {
                 items(items) {
@@ -173,4 +176,60 @@
         rule.onNodeWithTag("9")
             .assertIsDisplayed()
     }
+
+    @Test
+    fun adaptiveLazyGridFillsAllWidth() {
+        val items = (1..5).map { it.toString() }
+
+        rule.setContent {
+            LazyVerticalGrid(
+                cells = GridCells.Adaptive(130.dp),
+                modifier = Modifier.preferredHeight(100.dp).preferredWidth(300.dp)
+            ) {
+                items(items) {
+                    Spacer(Modifier.preferredHeight(101.dp).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(150.dp)
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("5")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun adaptiveLazyGridAtLeastOneColumn() {
+        val items = (1..3).map { it.toString() }
+
+        rule.setContent {
+            LazyVerticalGrid(
+                cells = GridCells.Adaptive(301.dp),
+                modifier = Modifier.preferredHeight(100.dp).preferredWidth(300.dp)
+            ) {
+                items(items) {
+                    Spacer(Modifier.preferredHeight(101.dp).testTag(it))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+    }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyItemStateRestoration.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyItemStateRestoration.kt
new file mode 100644
index 0000000..5dc3b6f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyItemStateRestoration.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.onDispose
+import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+
+class LazyItemStateRestoration {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun visibleItemsStateRestored() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        var counter2 = 100
+        var realState = arrayOf(0, 0, 0)
+        restorationTester.setContent {
+            LazyColumn {
+                item {
+                    realState[0] = rememberSavedInstanceState { counter0++ }
+                    Box(Modifier.size(1.dp))
+                }
+                items((1..2).toList()) {
+                    if (it == 1) {
+                        realState[1] = rememberSavedInstanceState { counter1++ }
+                    } else {
+                        realState[2] = rememberSavedInstanceState { counter2++ }
+                    }
+                    Box(Modifier.size(1.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+            realState = arrayOf(0, 0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            assertThat(realState[1]).isEqualTo(10)
+            assertThat(realState[2]).isEqualTo(100)
+        }
+    }
+
+    @Test
+    fun itemsStateRestoredWhenWeScrolledBackToIt() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        lateinit var state: LazyListState
+        var itemDisposed = false
+        var realState = 0
+        restorationTester.setContent {
+            LazyColumn(
+                Modifier.size(20.dp),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0..1).toList()) {
+                    if (it == 0) {
+                        realState = rememberSavedInstanceState { counter0++ }
+                        onDispose {
+                            itemDisposed = true
+                        }
+                    }
+                    Box(Modifier.size(30.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+            runBlocking {
+                state.snapToItemIndex(1, 5)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(itemDisposed).isEqualTo(true)
+            realState = 0
+            runBlocking {
+                state.snapToItemIndex(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun itemsStateRestoredWhenWeScrolledRestoredAndScrolledBackTo() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        var counter1 = 10
+        lateinit var state: LazyListState
+        var realState = arrayOf(0, 0)
+        restorationTester.setContent {
+            LazyColumn(
+                Modifier.size(20.dp),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0..1).toList()) {
+                    if (it == 0) {
+                        realState[0] = rememberSavedInstanceState { counter0++ }
+                    } else {
+                        realState[1] = rememberSavedInstanceState { counter1++ }
+                    }
+                    Box(Modifier.size(30.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+            runBlocking {
+                state.snapToItemIndex(1, 5)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[1]).isEqualTo(10)
+            realState = arrayOf(0, 0)
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(realState[1]).isEqualTo(10)
+            runBlocking {
+                state.snapToItemIndex(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState[0]).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun nestedLazy_itemsStateRestoredWhenWeScrolledBackToIt() {
+        val restorationTester = StateRestorationTester(rule)
+        var counter0 = 1
+        lateinit var state: LazyListState
+        var itemDisposed = false
+        var realState = 0
+        restorationTester.setContent {
+            LazyColumn(
+                Modifier.size(20.dp),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0..1).toList()) {
+                    if (it == 0) {
+                        LazyRow {
+                            item {
+                                realState = rememberSavedInstanceState { counter0++ }
+                                onDispose {
+                                    itemDisposed = true
+                                }
+                                Box(Modifier.size(30.dp))
+                            }
+                        }
+                    } else {
+                        Box(Modifier.size(30.dp))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+            runBlocking {
+                state.snapToItemIndex(1, 5)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(itemDisposed).isEqualTo(true)
+            realState = 0
+            runBlocking {
+                state.snapToItemIndex(0, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(realState).isEqualTo(1)
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
new file mode 100644
index 0000000..c5d040e
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyListLayoutInfoTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrect() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.size(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 4)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectAfterScroll() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.size(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.snapToItemIndex(1, 10)
+            }
+            state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1, startOffset = -10)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectWithSpacing() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyListState().also { state = it },
+                verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+                modifier = Modifier.size(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.size(itemSizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenWeScroll() {
+        lateinit var state: LazyListState
+        var currentInfo: LazyListLayoutInfo? = null
+        @Composable
+        fun observingFun() {
+            currentInfo = state.layoutInfo
+        }
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSizeDp * 3.5f)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.size(itemSizeDp))
+                }
+            }
+            observingFun()
+        }
+
+        rule.runOnIdle {
+            // empty it here and scrolling should invoke observingFun again
+            currentInfo = null
+            runBlocking {
+                state.snapToItemIndex(1, 0)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 4, startIndex = 1)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenResize() {
+        lateinit var state: LazyListState
+        var size by mutableStateOf(itemSizeDp * 2)
+        var currentInfo: LazyListLayoutInfo? = null
+        @Composable
+        fun observingFun() {
+            currentInfo = state.layoutInfo
+        }
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyListState().also { state = it }
+            ) {
+                item {
+                    Box(Modifier.size(size))
+                }
+            }
+            observingFun()
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+            currentInfo = null
+            size = itemSizeDp
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+        }
+    }
+
+    @Test
+    fun totalCountIsCorrect() {
+        var count by mutableStateOf(10)
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0 until count).toList()) {
+                    Box(Modifier.size(10.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+            count = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAreCorrect() {
+        val sizePx = 45
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                Modifier.size(sizeDp),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.size(sizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+        }
+    }
+
+    @Test
+    fun viewportOffsetsAreCorrectWithContentPadding() {
+        val sizePx = 45
+        val topPaddingPx = 10
+        val bottomPaddingPx = 15
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        val topPaddingDp = with(rule.density) { topPaddingPx.toDp() }
+        val bottomPaddingDp = with(rule.density) { bottomPaddingPx.toDp() }
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                Modifier.size(sizeDp),
+                contentPadding = PaddingValues(top = topPaddingDp, bottom = bottomPaddingDp),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.size(sizeDp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-topPaddingPx)
+            assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - topPaddingPx)
+        }
+    }
+
+    fun LazyListLayoutInfo.assertVisibleItems(
+        count: Int,
+        startIndex: Int = 0,
+        startOffset: Int = 0,
+        expectedSize: Int = itemSizePx,
+        spacing: Int = 0
+    ) {
+        assertThat(visibleItemsInfo.size).isEqualTo(count)
+        var currentIndex = startIndex
+        var currentOffset = startOffset
+        visibleItemsInfo.forEach {
+            assertThat(it.index).isEqualTo(currentIndex)
+            assertThat(it.offset).isEqualTo(currentOffset)
+            assertThat(it.size).isEqualTo(expectedSize)
+            currentIndex++
+            currentOffset += it.size + spacing
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
index c77dee6..a7d5427 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.lazy
 
 import androidx.compose.animation.core.snap
+import androidx.compose.foundation.animation.smoothScrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
@@ -68,11 +69,10 @@
         val smallPaddingSize = itemSize / 4
         val largePaddingSize = itemSize
         rule.setContent {
-            LazyColumnFor(
-                items = listOf(1),
-                state = rememberLazyListState().also { state = it },
+            LazyColumn(
                 modifier = Modifier.size(containerSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     start = smallPaddingSize,
                     top = largePaddingSize,
@@ -80,7 +80,9 @@
                     bottom = largePaddingSize
                 )
             ) {
-                Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+                items(listOf(1)) {
+                    Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+                }
             }
         }
 
@@ -101,17 +103,18 @@
     fun column_contentPaddingIsNotAffectingScrollPosition() {
         lateinit var state: LazyListState
         rule.setContent {
-            LazyColumnFor(
-                items = listOf(1),
-                state = rememberLazyListState().also { state = it },
+            LazyColumn(
                 modifier = Modifier.size(itemSize * 2)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     top = itemSize,
                     bottom = itemSize
                 )
             ) {
-                Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+                items(listOf(1)) {
+                    Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+                }
             }
         }
 
@@ -127,17 +130,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyColumnFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyColumn(
                 modifier = Modifier.size(padding * 2 + itemSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     top = padding,
                     bottom = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -167,17 +171,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyColumnFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyColumn(
                 modifier = Modifier.size(itemSize + padding * 2)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     top = padding,
                     bottom = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -201,17 +206,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyColumnFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyColumn(
                 modifier = Modifier.size(padding * 2 + itemSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     top = padding,
                     bottom = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -244,17 +250,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyColumnFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyColumn(
                 modifier = Modifier.size(padding * 2 + itemSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     top = padding,
                     bottom = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -275,8 +282,7 @@
     fun column_contentPaddingAndWrapContent() {
         rule.setContent {
             Box(modifier = Modifier.testTag(ContainerTag)) {
-                LazyColumnFor(
-                    items = listOf(1),
+                LazyColumn(
                     contentPadding = PaddingValues(
                         start = 2.dp,
                         top = 4.dp,
@@ -284,7 +290,9 @@
                         bottom = 8.dp
                     )
                 ) {
-                    Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    items(listOf(1)) {
+                        Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    }
                 }
             }
         }
@@ -306,8 +314,7 @@
     fun column_contentPaddingAndNoContent() {
         rule.setContent {
             Box(modifier = Modifier.testTag(ContainerTag)) {
-                LazyColumnFor(
-                    items = listOf(0),
+                LazyColumn(
                     contentPadding = PaddingValues(
                         start = 2.dp,
                         top = 4.dp,
@@ -315,6 +322,8 @@
                         bottom = 8.dp
                     )
                 ) {
+                    items(listOf(0)) {
+                    }
                 }
             }
         }
@@ -333,11 +342,10 @@
         val smallPaddingSize = itemSize / 4
         val largePaddingSize = itemSize
         rule.setContent {
-            LazyRowFor(
-                items = listOf(1),
-                state = rememberLazyListState().also { state = it },
+            LazyRow(
                 modifier = Modifier.size(containerSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     top = smallPaddingSize,
                     start = largePaddingSize,
@@ -345,7 +353,9 @@
                     end = largePaddingSize
                 )
             ) {
-                Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+                items(listOf(1)) {
+                    Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+                }
             }
         }
 
@@ -369,17 +379,18 @@
             50.dp.toIntPx().toDp()
         }
         rule.setContent {
-            LazyRowFor(
-                items = listOf(1),
-                state = rememberLazyListState().also { state = it },
+            LazyRow(
                 modifier = Modifier.size(itemSize * 2)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     start = itemSize,
                     end = itemSize
                 )
             ) {
-                Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+                items(listOf(1)) {
+                    Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+                }
             }
         }
 
@@ -395,17 +406,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyRowFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyRow(
                 modifier = Modifier.size(padding * 2 + itemSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     start = padding,
                     end = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -435,17 +447,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyRowFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyRow(
                 modifier = Modifier.size(itemSize + padding * 2)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     start = padding,
                     end = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -469,17 +482,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyRowFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyRow(
                 modifier = Modifier.size(padding * 2 + itemSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     start = padding,
                     end = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -512,17 +526,18 @@
         lateinit var state: LazyListState
         val padding = itemSize * 1.5f
         rule.setContent {
-            LazyRowFor(
-                items = (0..3).toList(),
-                state = rememberLazyListState().also { state = it },
+            LazyRow(
                 modifier = Modifier.size(padding * 2 + itemSize)
                     .testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it },
                 contentPadding = PaddingValues(
                     start = padding,
                     end = padding
                 )
             ) {
-                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                items((0..3).toList()) {
+                    Spacer(Modifier.size(itemSize).testTag(it.toString()))
+                }
             }
         }
 
@@ -543,8 +558,7 @@
     fun row_contentPaddingAndWrapContent() {
         rule.setContent {
             Box(modifier = Modifier.testTag(ContainerTag)) {
-                LazyRowFor(
-                    items = listOf(1),
+                LazyRow(
                     contentPadding = PaddingValues(
                         start = 2.dp,
                         top = 4.dp,
@@ -552,7 +566,9 @@
                         bottom = 8.dp
                     )
                 ) {
-                    Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    items(listOf(1)) {
+                        Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    }
                 }
             }
         }
@@ -574,8 +590,7 @@
     fun row_contentPaddingAndNoContent() {
         rule.setContent {
             Box(modifier = Modifier.testTag(ContainerTag)) {
-                LazyRowFor(
-                    items = listOf(0),
+                LazyRow(
                     contentPadding = PaddingValues(
                         start = 2.dp,
                         top = 4.dp,
@@ -583,6 +598,7 @@
                         bottom = 8.dp
                     )
                 ) {
+                    items(listOf(0)) {}
                 }
             }
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsIndexedTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsIndexedTest.kt
new file mode 100644
index 0000000..16bb089
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsIndexedTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.layout.preferredWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+import org.junit.Test
+
+class LazyListsIndexedTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun lazyColumnShowsIndexedItems() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            LazyColumn(Modifier.preferredHeight(200.dp)) {
+                itemsIndexed(items) { index, item ->
+                    Spacer(
+                        Modifier.preferredHeight(101.dp).fillParentMaxWidth()
+                            .testTag("$index-$item")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0-1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1-2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2-3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("3-4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun columnWithIndexesComposedWithCorrectIndexAndItem() {
+        val items = (0..1).map { it.toString() }
+
+        rule.setContent {
+            LazyColumn(Modifier.preferredHeight(200.dp)) {
+                itemsIndexed(items) { index, item ->
+                    BasicText("${index}x$item", Modifier.fillParentMaxWidth().height(100.dp))
+                }
+            }
+        }
+
+        rule.onNodeWithText("0x0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithText("1x1")
+            .assertTopPositionInRootIsEqualTo(100.dp)
+    }
+
+    @Test
+    fun lazyRowShowsIndexedItems() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            LazyRow(Modifier.preferredWidth(200.dp)) {
+                itemsIndexed(items) { index, item ->
+                    Spacer(
+                        Modifier.preferredWidth(101.dp).fillParentMaxHeight()
+                            .testTag("$index-$item")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0-1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("1-2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2-3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("3-4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun rowWithIndexesComposedWithCorrectIndexAndItem() {
+        val items = (0..1).map { it.toString() }
+
+        rule.setContent {
+            LazyRow(Modifier.preferredWidth(200.dp)) {
+                itemsIndexed(items) { index, item ->
+                    BasicText("${index}x$item", Modifier.fillParentMaxHeight().width(100.dp))
+                }
+            }
+        }
+
+        rule.onNodeWithText("0x0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithText("1x1")
+            .assertLeftPositionInRootIsEqualTo(100.dp)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsReverseLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsReverseLayoutTest.kt
new file mode 100644
index 0000000..9f02efa
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsReverseLayoutTest.kt
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.InternalLayoutApi
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Providers
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(InternalLayoutApi::class)
+class LazyListsReverseLayoutTest {
+
+    private val ContainerTag = "ContainerTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+    }
+
+    @Test
+    fun column_emitTwoElementsAsOneItem_positionedReversed() {
+        rule.setContent {
+            LazyColumn(
+                reverseLayout = true
+            ) {
+                item {
+                    Box(Modifier.size(itemSize).testTag("0"))
+                    Box(Modifier.size(itemSize).testTag("1"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_emitTwoItems_positionedReversed() {
+        rule.setContent {
+            LazyColumn(
+                reverseLayout = true
+            ) {
+                item {
+                    Box(Modifier.size(itemSize).testTag("0"))
+                }
+                item {
+                    Box(Modifier.size(itemSize).testTag("1"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_initialScrollPositionIs0() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun column_scrollInWrongDirectionDoesNothing() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        // we scroll down and as the scrolling is reversed it shouldn't affect anything
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(y = itemSize, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_scrollForwardHalfWay() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(y = -itemSize * 0.5f, density = rule.density)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(scrolled)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+    }
+
+    @Test
+    fun column_scrollForwardTillTheEnd() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumn(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        // we scroll a bit more than it is possible just to make sure we would stop correctly
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(y = -itemSize * 2.2f, density = rule.density)
+
+        rule.runOnIdle {
+            with(rule.density) {
+                val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+                    itemSize * state.firstVisibleItemIndex
+                assertThat(realOffset).isEqualTo(itemSize * 2)
+            }
+        }
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_emitTwoElementsAsOneItem_positionedReversed() {
+        rule.setContent {
+            LazyRow(
+                reverseLayout = true
+            ) {
+                item {
+                    Box(Modifier.size(itemSize).testTag("0"))
+                    Box(Modifier.size(itemSize).testTag("1"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_emitTwoItems_positionedReversed() {
+        rule.setContent {
+            LazyRow(
+                reverseLayout = true
+            ) {
+                item {
+                    Box(Modifier.size(itemSize).testTag("0"))
+                }
+                item {
+                    Box(Modifier.size(itemSize).testTag("1"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_initialScrollPositionIs0() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun row_scrollInWrongDirectionDoesNothing() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        // we scroll down and as the scrolling is reversed it shouldn't affect anything
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(x = itemSize, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_scrollForwardHalfWay() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(x = -itemSize * 0.5f, density = rule.density)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(scrolled)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+    }
+
+    @Test
+    fun row_scrollForwardTillTheEnd() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyRow(
+                reverseLayout = true,
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        // we scroll a bit more than it is possible just to make sure we would stop correctly
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(x = -itemSize * 2.2f, density = rule.density)
+
+        rule.runOnIdle {
+            with(rule.density) {
+                val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+                    itemSize * state.firstVisibleItemIndex
+                assertThat(realOffset).isEqualTo(itemSize * 2)
+            }
+        }
+
+        rule.onNodeWithTag("3")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+        rule.setContent {
+            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                LazyRow(
+                    reverseLayout = true
+                ) {
+                    item {
+                        Box(Modifier.size(itemSize).testTag("0"))
+                        Box(Modifier.size(itemSize).testTag("1"))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun row_rtl_emitTwoItems_positionedReversed() {
+        rule.setContent {
+            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                LazyRow(
+                    reverseLayout = true
+                ) {
+                    item {
+                        Box(Modifier.size(itemSize).testTag("0"))
+                    }
+                    item {
+                        Box(Modifier.size(itemSize).testTag("1"))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun row_rtl_scrollForwardHalfWay() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                LazyRow(
+                    reverseLayout = true,
+                    state = rememberLazyListState().also { state = it },
+                    modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+                ) {
+                    items((0..2).toList()) {
+                        Box(Modifier.size(itemSize).testTag("$it"))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(x = itemSize * 0.5f, density = rule.density)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(-scrolled)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt
index 6b5902a..fca6c58 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt
@@ -16,12 +16,14 @@
 
 package androidx.compose.foundation.lazy
 
-import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberScrollableController
+import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.TouchSlop
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.down
@@ -34,6 +36,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -46,21 +49,33 @@
     @get:Rule
     val rule = createComposeRule()
 
+    var expectedDragOffset = Float.MAX_VALUE
+
+    @Before
+    fun test() {
+        expectedDragOffset = with(rule.density) {
+            TouchSlop.toPx() + 20
+        }
+    }
+
     @Test
     fun column_nestedScrollingBackwardInitially() {
         val items = (1..3).toList()
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Vertical) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyColumnFor(
-                    items = items,
-                    modifier = Modifier.size(100.dp).testTag(LazyTag)
-                ) {
-                    Spacer(Modifier.size(50.dp).testTag("$it"))
+                LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+                    items(items) {
+                        Spacer(Modifier.size(50.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -83,15 +98,18 @@
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Vertical) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyColumnFor(
-                    items = items,
-                    modifier = Modifier.size(100.dp).testTag(LazyTag)
-                ) {
-                    Spacer(Modifier.size(50.dp).testTag("$it"))
+                LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+                    items(items) {
+                        Spacer(Modifier.size(50.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -109,12 +127,12 @@
             .performGesture {
                 draggedOffset = 0f
                 down(Offset(x = 10f, y = 10f))
-                moveBy(Offset(x = 0f, y = 50f))
+                moveBy(Offset(x = 0f, y = expectedDragOffset))
                 up()
             }
 
         rule.runOnIdle {
-            Truth.assertThat(draggedOffset).isEqualTo(50f)
+            Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
         }
     }
 
@@ -124,15 +142,18 @@
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Vertical) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyColumnFor(
-                    items = items,
-                    modifier = Modifier.size(100.dp).testTag(LazyTag)
-                ) {
-                    Spacer(Modifier.size(40.dp).testTag("$it"))
+                LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+                    items(items) {
+                        Spacer(Modifier.size(40.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -140,12 +161,12 @@
         rule.onNodeWithTag(LazyTag)
             .performGesture {
                 down(Offset(x = 10f, y = 10f))
-                moveBy(Offset(x = 0f, y = -50f))
+                moveBy(Offset(x = 0f, y = -expectedDragOffset))
                 up()
             }
 
         rule.runOnIdle {
-            Truth.assertThat(draggedOffset).isEqualTo(-50f)
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
         }
     }
 
@@ -155,15 +176,18 @@
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Vertical) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Vertical,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyColumnFor(
-                    items = items,
-                    modifier = Modifier.size(100.dp).testTag(LazyTag)
-                ) {
-                    Spacer(Modifier.size(50.dp).testTag("$it"))
+                LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+                    items(items) {
+                        Spacer(Modifier.size(50.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -176,12 +200,12 @@
             .performGesture {
                 draggedOffset = 0f
                 down(Offset(x = 10f, y = 10f))
-                moveBy(Offset(x = 0f, y = -50f))
+                moveBy(Offset(x = 0f, y = -expectedDragOffset))
                 up()
             }
 
         rule.runOnIdle {
-            Truth.assertThat(draggedOffset).isEqualTo(-50f)
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
         }
     }
 
@@ -191,15 +215,20 @@
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Horizontal) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyRowFor(
-                    items = items,
+                LazyRow(
                     modifier = Modifier.size(100.dp).testTag(LazyTag)
                 ) {
-                    Spacer(Modifier.size(50.dp).testTag("$it"))
+                    items(items) {
+                        Spacer(Modifier.size(50.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -207,12 +236,12 @@
         rule.onNodeWithTag(LazyTag)
             .performGesture {
                 down(Offset(x = 10f, y = 10f))
-                moveBy(Offset(x = 100f, y = 0f))
+                moveBy(Offset(x = expectedDragOffset, y = 0f))
                 up()
             }
 
         rule.runOnIdle {
-            Truth.assertThat(draggedOffset).isEqualTo(100f)
+            Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
         }
     }
 
@@ -222,15 +251,20 @@
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Horizontal) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyRowFor(
-                    items = items,
+                LazyRow(
                     modifier = Modifier.size(100.dp).testTag(LazyTag)
                 ) {
-                    Spacer(Modifier.size(50.dp).testTag("$it"))
+                    items(items) {
+                        Spacer(Modifier.size(50.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -248,12 +282,12 @@
             .performGesture {
                 draggedOffset = 0f
                 down(Offset(x = 10f, y = 10f))
-                moveBy(Offset(x = 50f, y = 0f))
+                moveBy(Offset(x = expectedDragOffset, y = 0f))
                 up()
             }
 
         rule.runOnIdle {
-            Truth.assertThat(draggedOffset).isEqualTo(50f)
+            Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
         }
     }
 
@@ -263,15 +297,20 @@
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Horizontal) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyRowFor(
-                    items = items,
+                LazyRow(
                     modifier = Modifier.size(100.dp).testTag(LazyTag)
                 ) {
-                    Spacer(Modifier.size(40.dp).testTag("$it"))
+                    items(items) {
+                        Spacer(Modifier.size(40.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -279,12 +318,12 @@
         rule.onNodeWithTag(LazyTag)
             .performGesture {
                 down(Offset(x = 10f, y = 10f))
-                moveBy(Offset(x = -50f, y = 0f))
+                moveBy(Offset(x = -expectedDragOffset, y = 0f))
                 up()
             }
 
         rule.runOnIdle {
-            Truth.assertThat(draggedOffset).isEqualTo(-50f)
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
         }
     }
 
@@ -294,15 +333,20 @@
         var draggedOffset = 0f
         rule.setContent {
             Box(
-                Modifier.draggable(Orientation.Horizontal) {
-                    draggedOffset += it
-                }
+                Modifier.scrollable(
+                    orientation = Orientation.Horizontal,
+                    controller = rememberScrollableController {
+                        draggedOffset += it
+                        it
+                    }
+                )
             ) {
-                LazyRowFor(
-                    items = items,
+                LazyRow(
                     modifier = Modifier.size(100.dp).testTag(LazyTag)
                 ) {
-                    Spacer(Modifier.size(50.dp).testTag("$it"))
+                    items(items) {
+                        Spacer(Modifier.size(50.dp).testTag("$it"))
+                    }
                 }
             }
         }
@@ -315,12 +359,12 @@
             .performGesture {
                 draggedOffset = 0f
                 down(Offset(x = 10f, y = 10f))
-                moveBy(Offset(x = -50f, y = 0f))
+                moveBy(Offset(x = -expectedDragOffset, y = 0f))
                 up()
             }
 
         rule.runOnIdle {
-            Truth.assertThat(draggedOffset).isEqualTo(-50f)
+            Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
         }
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
deleted file mode 100644
index 6540372..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
+++ /dev/null
@@ -1,839 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.preferredSize
-import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Providers
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.platform.AmbientLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyRowForTest {
-    private val LazyRowForTag = "LazyRowForTag"
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun lazyRowOnlyVisibleItemsAdded() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            Box(Modifier.preferredWidth(200.dp)) {
-                LazyRowFor(items) {
-                    Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag("1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("4")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun lazyRowScrollToShowItems123() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            Box(Modifier.preferredWidth(200.dp)) {
-                LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
-                    Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 50.dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("4")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun lazyRowScrollToHideFirstItem() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            Box(Modifier.preferredWidth(200.dp)) {
-                LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
-                    Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 102.dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun lazyRowScrollToShowItems234() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            Box(Modifier.preferredWidth(200.dp)) {
-                LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
-                    Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 150.dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("4")
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun lazyRowWrapsContent() = with(rule.density) {
-        val itemInsideLazyRow = "itemInsideLazyRow"
-        val itemOutsideLazyRow = "itemOutsideLazyRow"
-        var sameSizeItems by mutableStateOf(true)
-
-        rule.setContent {
-            Column {
-                LazyRowFor(
-                    items = listOf(1, 2),
-                    modifier = Modifier.testTag(LazyRowForTag)
-                ) {
-                    if (it == 1) {
-                        Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyRow))
-                    } else {
-                        Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
-                    }
-                }
-                Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyRow))
-            }
-        }
-
-        rule.onNodeWithTag(itemInsideLazyRow)
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag(itemOutsideLazyRow)
-            .assertIsDisplayed()
-
-        var lazyRowBounds = rule.onNodeWithTag(LazyRowForTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
-        assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
-
-        rule.runOnIdle {
-            sameSizeItems = false
-        }
-
-        rule.waitForIdle()
-
-        rule.onNodeWithTag(itemInsideLazyRow)
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag(itemOutsideLazyRow)
-            .assertIsDisplayed()
-
-        lazyRowBounds = rule.onNodeWithTag(LazyRowForTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
-        assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-        assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
-    }
-
-    private val firstItemTag = "firstItemTag"
-    private val secondItemTag = "secondItemTag"
-
-    private fun prepareLazyRowForAlignment(verticalGravity: Alignment.Vertical) {
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(1, 2),
-                modifier = Modifier.testTag(LazyRowForTag).height(100.dp),
-                verticalAlignment = verticalGravity
-            ) {
-                if (it == 1) {
-                    Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
-                } else {
-                    Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertIsDisplayed()
-
-        val lazyRowBounds = rule.onNodeWithTag(LazyRowForTag)
-            .getUnclippedBoundsInRoot()
-
-        with(rule.density) {
-            // Verify the height of the row
-            assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
-            assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
-        }
-    }
-
-    @Test
-    fun lazyRowAlignmentCenterVertically() {
-        prepareLazyRowForAlignment(Alignment.CenterVertically)
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertPositionInRootIsEqualTo(0.dp, 25.dp)
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertPositionInRootIsEqualTo(50.dp, 15.dp)
-    }
-
-    @Test
-    fun lazyRowAlignmentTop() {
-        prepareLazyRowForAlignment(Alignment.Top)
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertPositionInRootIsEqualTo(50.dp, 0.dp)
-    }
-
-    @Test
-    fun lazyRowAlignmentBottom() {
-        prepareLazyRowForAlignment(Alignment.Bottom)
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertPositionInRootIsEqualTo(0.dp, 50.dp)
-
-        rule.onNodeWithTag(secondItemTag)
-            .assertPositionInRootIsEqualTo(50.dp, 30.dp)
-    }
-
-    @Test
-    fun itemFillingParentWidth() {
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(100.dp)
-            .assertHeightIsEqualTo(50.dp)
-    }
-
-    @Test
-    fun itemFillingParentHeight() {
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(50.dp)
-            .assertHeightIsEqualTo(150.dp)
-    }
-
-    @Test
-    fun itemFillingParentSize() {
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(100.dp)
-            .assertHeightIsEqualTo(150.dp)
-    }
-
-    @Test
-    fun itemFillingParentWidthFraction() {
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxWidth(0.7f).height(50.dp).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(70.dp)
-            .assertHeightIsEqualTo(50.dp)
-    }
-
-    @Test
-    fun itemFillingParentHeightFraction() {
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.3f).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(50.dp)
-            .assertHeightIsEqualTo(45.dp)
-    }
-
-    @Test
-    fun itemFillingParentSizeFraction() {
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(0),
-                modifier = Modifier.size(width = 100.dp, height = 150.dp)
-            ) {
-                Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
-            }
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(50.dp)
-            .assertHeightIsEqualTo(75.dp)
-    }
-
-    @Test
-    fun itemFillingParentSizeParentResized() {
-        var parentSize by mutableStateOf(100.dp)
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(0),
-                modifier = Modifier.size(parentSize)
-            ) {
-                Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
-            }
-        }
-
-        rule.runOnIdle {
-            parentSize = 150.dp
-        }
-
-        rule.onNodeWithTag(firstItemTag)
-            .assertWidthIsEqualTo(150.dp)
-            .assertHeightIsEqualTo(150.dp)
-    }
-
-    @Test
-    fun scrollsLeftInRtl() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
-                Box(Modifier.preferredWidth(100.dp)) {
-                    LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
-                        Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                    }
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = (-150).dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun whenNotAnymoreAvailableItemWasDisplayed() {
-        var items by mutableStateOf((1..30).toList())
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 16-20
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 300.dp, density = rule.density)
-
-        rule.runOnIdle {
-            items = (1..10).toList()
-        }
-
-        // there is no item 16 anymore so we will just display the last items 6-10
-        rule.onNodeWithTag("6")
-            .assertLeftPositionIsAlmost(0.dp)
-    }
-
-    @Test
-    fun whenFewDisplayedItemsWereRemoved() {
-        var items by mutableStateOf((1..10).toList())
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 6-10
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 100.dp, density = rule.density)
-
-        rule.runOnIdle {
-            items = (1..8).toList()
-        }
-
-        // there are no more items 9 and 10, so we have to scroll back
-        rule.onNodeWithTag("4")
-            .assertLeftPositionIsAlmost(0.dp)
-    }
-
-    @Test
-    fun whenItemsBecameEmpty() {
-        var items by mutableStateOf((1..10).toList())
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyRowForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 2-6
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 20.dp, density = rule.density)
-
-        rule.runOnIdle {
-            items = emptyList()
-        }
-
-        // there are no more items so the LazyRow is zero sized
-        rule.onNodeWithTag(LazyRowForTag)
-            .assertWidthIsEqualTo(0.dp)
-            .assertHeightIsEqualTo(0.dp)
-
-        // and has no children
-        rule.onNodeWithTag("1")
-            .assertDoesNotExist()
-        rule.onNodeWithTag("2")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun scrollBackAndForth() {
-        val items by mutableStateOf((1..20).toList())
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // after scroll we will display items 6-10
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 100.dp, density = rule.density)
-
-        // and scroll back
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = (-100).dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertLeftPositionIsAlmost(0.dp)
-    }
-
-    @Test
-    fun tryToScrollBackwardWhenAlreadyOnTop() {
-        val items by mutableStateOf((1..20).toList())
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        // we already displaying the first item, so this should do nothing
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = (-50).dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertLeftPositionIsAlmost(0.dp)
-        rule.onNodeWithTag("5")
-            .assertLeftPositionIsAlmost(80.dp)
-    }
-
-    private fun SemanticsNodeInteraction.assertLeftPositionIsAlmost(expected: Dp) {
-        getUnclippedBoundsInRoot().left.assertIsEqualTo(expected, tolerance = 1.dp)
-    }
-
-    @Test
-    fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
-        val items = listOf(NotStable(1), NotStable(2))
-        var firstItemRecomposed = 0
-        var secondItemRecomposed = 0
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
-            ) {
-                if (it.count == 1) {
-                    firstItemRecomposed++
-                } else {
-                    secondItemRecomposed++
-                }
-                Spacer(Modifier.size(75.dp))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(firstItemRecomposed).isEqualTo(1)
-            assertThat(secondItemRecomposed).isEqualTo(1)
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = (50).dp, density = rule.density)
-
-        rule.runOnIdle {
-            assertThat(firstItemRecomposed).isEqualTo(1)
-            assertThat(secondItemRecomposed).isEqualTo(1)
-        }
-    }
-
-    @Test
-    fun onlyOneMeasurePassForScrollEvent() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        val initialMeasurePasses = state.numMeasurePasses
-
-        rule.runOnIdle {
-            with(rule.density) {
-                state.onScroll(-110.dp.toPx())
-            }
-        }
-
-        rule.waitForIdle()
-
-        assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
-    }
-
-    @Test
-    fun stateUpdatedAfterScroll() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 30.dp, density = rule.density)
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
-
-            with(rule.density) {
-                // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
-                //  number of pixels
-                val expectedOffset = 10.dp.toIntPx()
-                val tolerance = 2.dp.toIntPx()
-                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
-            }
-        }
-    }
-
-    @Test
-    fun stateUpdatedAfterScrollWithinTheSameItem() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 10.dp, density = rule.density)
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
-            with(rule.density) {
-                val expectedOffset = 10.dp.toIntPx()
-                val tolerance = 2.dp.toIntPx()
-                assertThat(state.firstVisibleItemScrollOffset)
-                    .isEqualTo(expectedOffset, tolerance)
-            }
-        }
-    }
-
-    @Test
-    fun initialScrollIsApplied() {
-        val items by mutableStateOf((0..20).toList())
-        lateinit var state: LazyListState
-        val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
-        rule.setContent {
-            state = rememberLazyListState(2, expectedOffset)
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(state.firstVisibleItemIndex).isEqualTo(2)
-            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
-        }
-
-        rule.onNodeWithTag("2")
-            .assertLeftPositionInRootIsEqualTo((-10).dp)
-    }
-
-    @Test
-    fun stateIsRestored() {
-        val restorationTester = StateRestorationTester(rule)
-        val items by mutableStateOf((1..20).toList())
-        var state: LazyListState? = null
-        restorationTester.setContent {
-            state = rememberLazyListState()
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
-                state = state!!
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 30.dp, density = rule.density)
-
-        val (index, scrollOffset) = rule.runOnIdle {
-            state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
-        }
-
-        state = null
-
-        restorationTester.emulateSavedInstanceStateRestore()
-
-        rule.runOnIdle {
-            assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
-            assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
-        }
-    }
-
-    @Test
-    fun snapToItemIndex() {
-        val items by mutableStateOf((1..20).toList())
-        lateinit var state: LazyListState
-        rule.setContent {
-            state = rememberLazyListState()
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
-                state = state
-            ) {
-                Spacer(Modifier.size(20.dp).testTag("$it"))
-            }
-        }
-
-        rule.runOnIdle {
-            runBlocking {
-                state.snapToItemIndex(3, 10)
-            }
-            assertThat(state.firstVisibleItemIndex).isEqualTo(3)
-            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
-        }
-    }
-
-    @Test
-    fun itemsAreNotRedrawnDuringScroll() {
-        val items = (0..20).toList()
-        val redrawCount = Array(6) { 0 }
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
-            ) {
-                Spacer(
-                    Modifier.size(20.dp)
-                        .drawBehind { redrawCount[it]++ }
-                )
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 10.dp, density = rule.density)
-
-        rule.runOnIdle {
-            redrawCount.forEachIndexed { index, i ->
-                Truth.assertWithMessage("Item with index $index was redrawn $i times")
-                    .that(i).isEqualTo(1)
-            }
-        }
-    }
-
-    @Test
-    fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
-        val items = (0..1).toList()
-        val redrawCount = Array(2) { 0 }
-        var stateUsedInDrawScope by mutableStateOf(false)
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
-            ) {
-                Spacer(
-                    Modifier.size(50.dp)
-                        .drawBehind {
-                            redrawCount[it]++
-                            if (it == 1) {
-                                stateUsedInDrawScope.hashCode()
-                            }
-                        }
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            stateUsedInDrawScope = true
-        }
-
-        rule.runOnIdle {
-            Truth.assertWithMessage("First items is not expected to be redrawn")
-                .that(redrawCount[0]).isEqualTo(1)
-            Truth.assertWithMessage("Second items is expected to be redrawn")
-                .that(redrawCount[1]).isEqualTo(2)
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
index fe720d7..ea14a36 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
@@ -16,17 +16,45 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.animation.smoothScrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.layout.preferredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Providers
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -34,83 +62,12 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class LazyRowTest {
-    private val LazyRowTag = "LazyRowTag"
+    private val LazyListTag = "LazyListTag"
 
     @get:Rule
     val rule = createComposeRule()
 
     @Test
-    fun lazyRowShowsItem() {
-        val itemTestTag = "itemTestTag"
-
-        rule.setContent {
-            LazyRow {
-                item {
-                    Spacer(
-                        Modifier.preferredWidth(10.dp).fillParentMaxHeight().testTag(itemTestTag)
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag(itemTestTag)
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun lazyRowShowsItems() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyRow(Modifier.preferredWidth(200.dp)) {
-                items(items) {
-                    Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag("1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("4")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun lazyRowShowsIndexedItems() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyRow(Modifier.preferredWidth(200.dp)) {
-                itemsIndexed(items) { index, item ->
-                    Spacer(
-                        Modifier.preferredWidth(101.dp).fillParentMaxHeight()
-                            .testTag("$index-$item")
-                    )
-                }
-            }
-        }
-
-        rule.onNodeWithTag("0-1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("1-2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2-3")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("3-4")
-            .assertDoesNotExist()
-    }
-
-    @Test
     fun lazyRowShowsCombinedItems() {
         val itemTestTag = "itemTestTag"
         val items = listOf(1, 2).map { it.toString() }
@@ -155,59 +112,6 @@
     }
 
     @Test
-    fun lazyRowShowsItemsOnScroll() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyRow(Modifier.preferredWidth(200.dp).testTag(LazyRowTag)) {
-                items(items) {
-                    Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowTag)
-            .scrollBy(x = 50.dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("4")
-            .assertDoesNotExist()
-    }
-
-    @Test
-    fun lazyRowScrollHidesItem() {
-        val items = (1..4).map { it.toString() }
-
-        rule.setContent {
-            LazyRow(Modifier.preferredWidth(200.dp).testTag(LazyRowTag)) {
-                items(items) {
-                    Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
-                }
-            }
-        }
-
-        rule.onNodeWithTag(LazyRowTag)
-            .scrollBy(x = 103.dp, density = rule.density)
-
-        rule.onNodeWithTag("1")
-            .assertDoesNotExist()
-
-        rule.onNodeWithTag("2")
-            .assertIsDisplayed()
-
-        rule.onNodeWithTag("3")
-            .assertIsDisplayed()
-    }
-
-    @Test
     fun lazyRowAllowEmptyListItems() {
         val itemTag = "itemTag"
 
@@ -253,4 +157,838 @@
         rule.onNodeWithTag("3")
             .assertDoesNotExist()
     }
-}
\ No newline at end of file
+
+    @Test
+    fun lazyRowOnlyVisibleItemsAdded() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            Box(Modifier.preferredWidth(200.dp)) {
+                LazyRow {
+                    items(items) {
+                        Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun lazyRowScrollToShowItems123() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            Box(Modifier.preferredWidth(200.dp)) {
+                LazyRow(Modifier.testTag(LazyListTag)) {
+                    items(items) {
+                        Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 50.dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("4")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun lazyRowScrollToHideFirstItem() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            Box(Modifier.preferredWidth(200.dp)) {
+                LazyRow(Modifier.testTag(LazyListTag)) {
+                    items(items) {
+                        Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 102.dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun lazyRowScrollToShowItems234() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            Box(Modifier.preferredWidth(200.dp)) {
+                LazyRow(Modifier.testTag(LazyListTag)) {
+                    items(items) {
+                        Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 150.dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun lazyRowWrapsContent() = with(rule.density) {
+        val itemInsideLazyRow = "itemInsideLazyRow"
+        val itemOutsideLazyRow = "itemOutsideLazyRow"
+        var sameSizeItems by mutableStateOf(true)
+
+        rule.setContent {
+            Column {
+                LazyRow(Modifier.testTag(LazyListTag)) {
+                    items(listOf(1, 2)) {
+                        if (it == 1) {
+                            Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyRow))
+                        } else {
+                            Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
+                        }
+                    }
+                }
+                Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyRow))
+            }
+        }
+
+        rule.onNodeWithTag(itemInsideLazyRow)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(itemOutsideLazyRow)
+            .assertIsDisplayed()
+
+        var lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+        assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
+
+        rule.runOnIdle {
+            sameSizeItems = false
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(itemInsideLazyRow)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(itemOutsideLazyRow)
+            .assertIsDisplayed()
+
+        lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
+        assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+        assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
+    }
+
+    private val firstItemTag = "firstItemTag"
+    private val secondItemTag = "secondItemTag"
+
+    private fun prepareLazyRowForAlignment(verticalGravity: Alignment.Vertical) {
+        rule.setContent {
+            LazyRow(
+                Modifier.testTag(LazyListTag).height(100.dp),
+                verticalAlignment = verticalGravity
+            ) {
+                items(listOf(1, 2)) {
+                    if (it == 1) {
+                        Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
+                    } else {
+                        Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertIsDisplayed()
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertIsDisplayed()
+
+        val lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+            .getUnclippedBoundsInRoot()
+
+        with(rule.density) {
+            // Verify the height of the row
+            assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+            assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+        }
+    }
+
+    @Test
+    fun lazyRowAlignmentCenterVertically() {
+        prepareLazyRowForAlignment(Alignment.CenterVertically)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 25.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 15.dp)
+    }
+
+    @Test
+    fun lazyRowAlignmentTop() {
+        prepareLazyRowForAlignment(Alignment.Top)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+    }
+
+    @Test
+    fun lazyRowAlignmentBottom() {
+        prepareLazyRowForAlignment(Alignment.Bottom)
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+
+        rule.onNodeWithTag(secondItemTag)
+            .assertPositionInRootIsEqualTo(50.dp, 30.dp)
+    }
+
+    @Test
+    fun itemFillingParentWidth() {
+        rule.setContent {
+            LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeight() {
+        rule.setContent {
+            LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentSize() {
+        rule.setContent {
+            LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(100.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun itemFillingParentWidthFraction() {
+        rule.setContent {
+            LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxWidth(0.7f).height(50.dp).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(70.dp)
+            .assertHeightIsEqualTo(50.dp)
+    }
+
+    @Test
+    fun itemFillingParentHeightFraction() {
+        rule.setContent {
+            LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.3f).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(45.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeFraction() {
+        rule.setContent {
+            LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(75.dp)
+    }
+
+    @Test
+    fun itemFillingParentSizeParentResized() {
+        var parentSize by mutableStateOf(100.dp)
+        rule.setContent {
+            LazyRow(Modifier.size(parentSize)) {
+                items(listOf(0)) {
+                    Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            parentSize = 150.dp
+        }
+
+        rule.onNodeWithTag(firstItemTag)
+            .assertWidthIsEqualTo(150.dp)
+            .assertHeightIsEqualTo(150.dp)
+    }
+
+    @Test
+    fun scrollsLeftInRtl() {
+        val items = (1..4).map { it.toString() }
+
+        rule.setContent {
+            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                Box(Modifier.preferredWidth(100.dp)) {
+                    LazyRow(Modifier.testTag(LazyListTag)) {
+                        items(items) {
+                            Spacer(
+                                Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it)
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = (-150).dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun whenNotAnymoreAvailableItemWasDisplayed() {
+        var items by mutableStateOf((1..30).toList())
+        rule.setContent {
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 16-20
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 300.dp, density = rule.density)
+
+        rule.runOnIdle {
+            items = (1..10).toList()
+        }
+
+        // there is no item 16 anymore so we will just display the last items 6-10
+        rule.onNodeWithTag("6")
+            .assertLeftPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun whenFewDisplayedItemsWereRemoved() {
+        var items by mutableStateOf((1..10).toList())
+        rule.setContent {
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 6-10
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 100.dp, density = rule.density)
+
+        rule.runOnIdle {
+            items = (1..8).toList()
+        }
+
+        // there are no more items 9 and 10, so we have to scroll back
+        rule.onNodeWithTag("4")
+            .assertLeftPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun whenItemsBecameEmpty() {
+        var items by mutableStateOf((1..10).toList())
+        rule.setContent {
+            LazyRow(Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 2-6
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 20.dp, density = rule.density)
+
+        rule.runOnIdle {
+            items = emptyList()
+        }
+
+        // there are no more items so the LazyRow is zero sized
+        rule.onNodeWithTag(LazyListTag)
+            .assertWidthIsEqualTo(0.dp)
+            .assertHeightIsEqualTo(0.dp)
+
+        // and has no children
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("2")
+            .assertDoesNotExist()
+    }
+
+    @Test
+    fun scrollBackAndForth() {
+        val items by mutableStateOf((1..20).toList())
+        rule.setContent {
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // after scroll we will display items 6-10
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 100.dp, density = rule.density)
+
+        // and scroll back
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = (-100).dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionIsAlmost(0.dp)
+    }
+
+    @Test
+    fun tryToScrollBackwardWhenAlreadyOnTop() {
+        val items by mutableStateOf((1..20).toList())
+        rule.setContent {
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        // we already displaying the first item, so this should do nothing
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = (-50).dp, density = rule.density)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionIsAlmost(0.dp)
+        rule.onNodeWithTag("5")
+            .assertLeftPositionIsAlmost(80.dp)
+    }
+
+    private fun SemanticsNodeInteraction.assertLeftPositionIsAlmost(expected: Dp) {
+        getUnclippedBoundsInRoot().left.assertIsEqualTo(expected, tolerance = 1.dp)
+    }
+
+    @Test
+    fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
+        val items = listOf(NotStable(1), NotStable(2))
+        var firstItemRecomposed = 0
+        var secondItemRecomposed = 0
+        rule.setContent {
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    if (it.count == 1) {
+                        firstItemRecomposed++
+                    } else {
+                        secondItemRecomposed++
+                    }
+                    Spacer(Modifier.size(75.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(firstItemRecomposed).isEqualTo(1)
+            assertThat(secondItemRecomposed).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = (50).dp, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(firstItemRecomposed).isEqualTo(1)
+            assertThat(secondItemRecomposed).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun onlyOneMeasurePassForScrollEvent() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyRow(Modifier.size(100.dp), state = state) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        val initialMeasurePasses = state.numMeasurePasses
+
+        rule.runOnIdle {
+            with(rule.density) {
+                state.onScroll(-110.dp.toPx())
+            }
+        }
+
+        rule.waitForIdle()
+
+        assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
+    }
+
+    @Test
+    fun stateUpdatedAfterScroll() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyRow(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 30.dp, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+
+            with(rule.density) {
+                // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
+                //  number of pixels
+                val expectedOffset = 10.dp.toIntPx()
+                val tolerance = 2.dp.toIntPx()
+                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
+            }
+        }
+    }
+
+    @Test
+    fun stateUpdatedAfterScrollWithinTheSameItem() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyRow(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 10.dp, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) {
+                val expectedOffset = 10.dp.toIntPx()
+                val tolerance = 2.dp.toIntPx()
+                assertThat(state.firstVisibleItemScrollOffset)
+                    .isEqualTo(expectedOffset, tolerance)
+            }
+        }
+    }
+
+    @Test
+    fun initialScrollIsApplied() {
+        val items by mutableStateOf((0..20).toList())
+        lateinit var state: LazyListState
+        val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
+        rule.setContent {
+            state = rememberLazyListState(2, expectedOffset)
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag), state = state) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo((-10).dp)
+    }
+
+    @Test
+    fun stateIsRestored() {
+        val restorationTester = StateRestorationTester(rule)
+        val items by mutableStateOf((1..20).toList())
+        var state: LazyListState? = null
+        restorationTester.setContent {
+            state = rememberLazyListState()
+            LazyRow(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state!!
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 30.dp, density = rule.density)
+
+        val (index, scrollOffset) = rule.runOnIdle {
+            state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
+        }
+
+        state = null
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
+            assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
+        }
+    }
+
+    @Test
+    fun snapToItemIndex() {
+        val items by mutableStateOf((1..20).toList())
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            LazyRow(
+                Modifier.size(100.dp).testTag(LazyListTag),
+                state = state
+            ) {
+                items(items) {
+                    Spacer(Modifier.size(20.dp).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.snapToItemIndex(3, 10)
+            }
+            assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+        }
+    }
+
+    @Test
+    fun itemsAreNotRedrawnDuringScroll() {
+        val items = (0..20).toList()
+        val redrawCount = Array(6) { 0 }
+        rule.setContent {
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(
+                        Modifier.size(20.dp)
+                            .drawBehind { redrawCount[it]++ }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(LazyListTag)
+            .scrollBy(x = 10.dp, density = rule.density)
+
+        rule.runOnIdle {
+            redrawCount.forEachIndexed { index, i ->
+                Truth.assertWithMessage("Item with index $index was redrawn $i times")
+                    .that(i).isEqualTo(1)
+            }
+        }
+    }
+
+    @Test
+    fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+        val items = (0..1).toList()
+        val redrawCount = Array(2) { 0 }
+        var stateUsedInDrawScope by mutableStateOf(false)
+        rule.setContent {
+            LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+                items(items) {
+                    Spacer(
+                        Modifier.size(50.dp)
+                            .drawBehind {
+                                redrawCount[it]++
+                                if (it == 1) {
+                                    stateUsedInDrawScope.hashCode()
+                                }
+                            }
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            stateUsedInDrawScope = true
+        }
+
+        rule.runOnIdle {
+            Truth.assertWithMessage("First items is not expected to be redrawn")
+                .that(redrawCount[0]).isEqualTo(1)
+            Truth.assertWithMessage("Second items is expected to be redrawn")
+                .that(redrawCount[1]).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+        val items = (0..1).toList()
+        val itemSize = with(rule.density) { 30.toDp() }
+        val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyRow(
+                Modifier.width(itemSizeMinusOne).testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items(items) {
+                    Spacer(
+                        if (it == 0) {
+                            Modifier.height(30.dp).width(itemSizeMinusOne)
+                        } else {
+                            Modifier.height(20.dp).width(itemSize)
+                        }
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag(LazyListTag)
+            .assertHeightIsEqualTo(20.dp)
+    }
+
+    @Test
+    fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+        val items = (0..2).toList()
+        val itemSize = with(rule.density) { 30.toDp() }
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyRow(
+                Modifier.width(itemSize * 1.75f).testTag(LazyListTag),
+                state = rememberLazyListState().also { state = it }
+            ) {
+                items(items) {
+                    Spacer(
+                        if (it == 0) {
+                            Modifier.height(30.dp).width(itemSize / 2)
+                        } else if (it == 1) {
+                            Modifier.height(20.dp).width(itemSize / 2)
+                        } else {
+                            Modifier.height(20.dp).width(itemSize)
+                        }
+                    )
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag(LazyListTag)
+            .assertHeightIsEqualTo(30.dp)
+    }
+
+    private fun LazyListState.scrollBy(offset: Dp) {
+        runBlocking {
+            smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt
index 8f60ce8c..0af07dd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.lazy
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.animation.smoothScrollBy
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.layout.preferredWidth
@@ -36,7 +37,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
-import kotlin.math.roundToInt
 
 @MediumTest
 @OptIn(ExperimentalTesting::class)
@@ -84,18 +84,24 @@
         }
         assertThat(state.firstVisibleItemIndex).isEqualTo(3)
         assertThat(state.firstVisibleItemScrollOffset)
-            .isEqualTo(with(rule.density) { 17.dp.toPx().roundToInt() })
+            .isEqualTo(
+                with(rule.density) { 320.dp.toIntPx() - 101.dp.toIntPx() * 3 }
+            )
     }
 
     @Composable
     private fun TestContent() {
         if (vertical) {
-            LazyColumnFor(items, Modifier.preferredHeight(300.dp), state) {
-                ItemContent()
+            LazyColumn(Modifier.preferredHeight(300.dp), state) {
+                items(items) {
+                    ItemContent()
+                }
             }
         } else {
-            LazyRowFor(items, Modifier.preferredWidth(300.dp), state) {
-                ItemContent()
+            LazyRow(Modifier.preferredWidth(300.dp), state) {
+                items(items) {
+                    ItemContent()
+                }
             }
         }
     }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index 441aa85..c1ccc04 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -17,9 +17,8 @@
 package androidx.compose.foundation.text
 
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -48,8 +47,7 @@
 
 @OptIn(
     ExperimentalTextApi::class,
-    InternalTextApi::class,
-    ExperimentalFocus::class
+    InternalTextApi::class
 )
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -82,7 +80,7 @@
                 imeOptions = imeOptions,
                 modifier = Modifier
                     .testTag(testTag)
-                    .focusObserver { focused = it.isFocused },
+                    .onFocusChanged { focused = it.isFocused },
                 onValueChange = {}
             )
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
new file mode 100644
index 0000000..3acb687
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.os.Build
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsetsAnimation
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focusRequester
+import androidx.compose.ui.platform.AmbientFocusManager
+import androidx.compose.ui.platform.AmbientView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(InternalTextApi::class)
+class CoreTextFieldSoftKeyboardTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun keyboardShownOnInitialClick() {
+        // Arrange.
+        lateinit var view: View
+        rule.setContent {
+            view = AmbientView.current
+            CoreTextField(
+                value = TextFieldValue("Hello"),
+                onValueChange = {},
+                modifier = Modifier.testTag("TextField1")
+            )
+        }
+        view.ensureKeyboardIsHidden()
+
+        // Act.
+        val isSoftKeyboardShown = view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+            rule.onNodeWithTag("TextField1").performClick()
+        }
+
+        // Assert.
+        assertThat(isSoftKeyboardShown).isTrue()
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun keyboardShownOnInitialFocus() {
+        // Arrange.
+        val focusRequester = FocusRequester()
+        lateinit var view: View
+        rule.setContent {
+            view = AmbientView.current
+            CoreTextField(
+                value = TextFieldValue("Hello"),
+                onValueChange = {},
+                modifier = Modifier.focusRequester(focusRequester)
+            )
+        }
+        view.ensureKeyboardIsHidden()
+
+        // Act.
+        val isSoftKeyboardShown = view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+            rule.runOnIdle { focusRequester.requestFocus() }
+        }
+
+        // Assert.
+        assertThat(isSoftKeyboardShown).isTrue()
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun keyboardHiddenWhenFocusIsLost() {
+        // Arrange.
+        lateinit var focusManager: FocusManager
+        lateinit var view: View
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            view = AmbientView.current
+            focusManager = AmbientFocusManager.current
+            CoreTextField(
+                value = TextFieldValue("Hello"),
+                onValueChange = {},
+                modifier = Modifier.focusRequester(focusRequester)
+            )
+        }
+        view.ensureKeyboardIsHidden()
+        // Request focus and wait for keyboard.
+        view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+            rule.runOnIdle { focusRequester.requestFocus() }
+        }
+
+        // Act.
+        val isSoftKeyboardHidden = view.runAndWaitUntil({ !view.isSoftwareKeyboardShown() }) {
+            rule.runOnIdle { focusManager.clearFocus() }
+        }
+
+        // Assert.
+        assertThat(isSoftKeyboardHidden).isTrue()
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun keyboardShownAfterDismissingKeyboardAndClickingAgain() {
+        // Arrange.
+        lateinit var view: View
+        rule.setContent {
+            view = AmbientView.current
+            CoreTextField(
+                value = TextFieldValue("Hello"),
+                onValueChange = {},
+                modifier = Modifier.testTag("TextField1")
+            )
+        }
+        view.ensureKeyboardIsHidden()
+        view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+            rule.onNodeWithTag("TextField1").performClick()
+        }
+        view.runAndWaitUntil({ !view.isSoftwareKeyboardShown() }) {
+            rule.runOnIdle { view.hideKeyboard() }
+        }
+
+        // Act.
+        val isSoftKeyboardVisible = view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+            rule.onNodeWithTag("TextField1").performClick()
+        }
+
+        // Assert.
+        assertThat(isSoftKeyboardVisible).isTrue()
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    fun keyboardStaysVisibleWhenMovingFromOneTextFieldToAnother() {
+        // Arrange.
+        val focusRequester1 = FocusRequester()
+        val focusRequester2 = FocusRequester()
+        lateinit var view: View
+        rule.setContent {
+            view = AmbientView.current
+            Column {
+                CoreTextField(
+                    value = TextFieldValue("Hello"),
+                    onValueChange = {},
+                    modifier = Modifier.focusRequester(focusRequester1)
+                )
+                CoreTextField(
+                    value = TextFieldValue("Hello"),
+                    onValueChange = {},
+                    modifier = Modifier.focusRequester(focusRequester2)
+                )
+            }
+        }
+        view.ensureKeyboardIsHidden()
+        view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+            rule.runOnIdle { focusRequester1.requestFocus() }
+        }
+
+        // Act.
+        val wasKeyboardHidden = view.runAndWaitUntil({ !view.isSoftwareKeyboardShown() }) {
+            rule.runOnIdle { focusRequester2.requestFocus() }
+        }
+
+        // Assert.
+        assertThat(wasKeyboardHidden).isFalse()
+    }
+
+    @RequiresApi(Build.VERSION_CODES.R)
+    private fun View.runAndWaitUntil(condition: () -> Boolean, block: () -> Unit): Boolean {
+        val latch = CountDownLatch(1)
+        rule.runOnIdle {
+            rootView.setWindowInsetsAnimationCallback(
+                InsetAnimationCallback {
+                    if (condition()) { latch.countDown() }
+                }
+            )
+        }
+        rule.waitForIdle()
+        block()
+        rule.waitForIdle()
+        return latch.await(15L, TimeUnit.SECONDS)
+    }
+
+    // We experienced some flakiness in tests if the keyboard was visible at the start of the test.
+    // This function makes sure the keyboard is hidden at the start of every test.
+    @RequiresApi(Build.VERSION_CODES.R)
+    private fun View.ensureKeyboardIsHidden() {
+        rule.waitForIdle()
+        if (isSoftwareKeyboardShown()) {
+            runAndWaitUntil({ !isSoftwareKeyboardShown() }) {
+                rule.runOnIdle { hideKeyboard() }
+            }
+        }
+        rule.waitForIdle()
+    }
+
+    @RequiresApi(Build.VERSION_CODES.R)
+    private class InsetAnimationCallback(val block: () -> Unit) :
+        WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+
+        override fun onProgress(
+            insets: WindowInsets,
+            runningAnimations: MutableList<WindowInsetsAnimation>
+        ) = insets
+
+        override fun onEnd(animation: WindowInsetsAnimation) {
+            block()
+            super.onEnd(animation)
+        }
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private fun View.isSoftwareKeyboardShown(): Boolean {
+    checkNotNull(rootWindowInsets)
+    return rootWindowInsets.isVisible(WindowInsets.Type.ime())
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private fun View.hideKeyboard() {
+    windowInsetsController?.hide(WindowInsets.Type.ime())
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
new file mode 100644
index 0000000..7d2d992
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.Interaction
+import androidx.compose.foundation.InteractionState
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focusRequester
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.cancel
+import androidx.compose.ui.test.center
+import androidx.compose.ui.test.down
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.moveBy
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performGesture
+import androidx.compose.ui.test.up
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(InternalTextApi::class)
+class TextFieldInteractionsTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    val testTag = "textField"
+
+    @Test
+    fun coreTextField_interaction_pressed() {
+        val state = mutableStateOf(TextFieldValue(""))
+        val interactionState = InteractionState()
+        rule.setContent {
+            BasicTextField(
+                modifier = Modifier.testTag(testTag),
+                value = state.value,
+                onValueChange = { state.value = it },
+                interactionState = interactionState
+            )
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Pressed)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                down(center)
+            }
+        assertThat(interactionState.value).contains(Interaction.Pressed)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                up()
+            }
+        assertThat(interactionState.value).doesNotContain(Interaction.Pressed)
+    }
+
+    @Test
+    fun coreTextField_interaction_pressed_removedWhenCancelled() {
+        val state = mutableStateOf(TextFieldValue(""))
+        val interactionState = InteractionState()
+        rule.setContent {
+            BasicTextField(
+                modifier = Modifier.testTag(testTag),
+                value = state.value,
+                onValueChange = { state.value = it },
+                interactionState = interactionState
+            )
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Pressed)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                down(center)
+            }
+        assertThat(interactionState.value).contains(Interaction.Pressed)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                cancel()
+            }
+        assertThat(interactionState.value).doesNotContain(Interaction.Pressed)
+    }
+
+    @Test
+    fun coreTextField_interaction_focused() {
+        val state = mutableStateOf(TextFieldValue(""))
+        val interactionState = InteractionState()
+        val otherRequester = FocusRequester()
+        rule.setContent {
+            BasicTextField(
+                modifier = Modifier.testTag(testTag),
+                value = state.value,
+                onValueChange = { state.value = it },
+                interactionState = interactionState
+            )
+            Box(
+                modifier = Modifier.size(10.dp).focusRequester(otherRequester).focusable(),
+            )
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Focused)
+        rule.onNodeWithTag(testTag)
+            .performClick()
+        assertThat(interactionState.value).contains(Interaction.Focused)
+        rule.runOnIdle {
+            // request focus on the box so TextField will lose it
+            otherRequester.requestFocus()
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Focused)
+    }
+
+    @Test
+    fun coreTextField_interaction_horizontally_dragged() {
+        val state = mutableStateOf(TextFieldValue("test ".repeat(100)))
+        val interactionState = InteractionState()
+        rule.setContent {
+            BasicTextField(
+                modifier = Modifier.testTag(testTag),
+                value = state.value,
+                singleLine = true,
+                onValueChange = { state.value = it },
+                interactionState = interactionState
+            )
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                down(center)
+                moveBy(Offset(x = 100f, y = 0f))
+            }
+        assertThat(interactionState.value).contains(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                up()
+            }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+    }
+
+    @Test
+    fun coreTextField_interaction_dragged_horizontally_cancelled() {
+        val state = mutableStateOf(TextFieldValue("test ".repeat(100)))
+        val interactionState = InteractionState()
+        rule.setContent {
+            BasicTextField(
+                modifier = Modifier.testTag(testTag),
+                value = state.value,
+                singleLine = true,
+                onValueChange = { state.value = it },
+                interactionState = interactionState
+            )
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                down(center)
+                moveBy(Offset(x = 100f, y = 0f))
+            }
+        assertThat(interactionState.value).contains(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                cancel()
+            }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+    }
+
+    @Test
+    fun coreTextField_interaction_vertically_dragged() {
+        val state = mutableStateOf(TextFieldValue("test\n".repeat(10)))
+        val interactionState = InteractionState()
+        rule.setContent {
+            BasicTextField(
+                modifier = Modifier.size(50.dp).testTag(testTag),
+                value = state.value,
+                maxLines = 3,
+                onValueChange = { state.value = it },
+                interactionState = interactionState
+            )
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                down(center)
+                moveBy(Offset(x = 0f, y = 150f))
+            }
+        assertThat(interactionState.value).contains(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                up()
+            }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+    }
+
+    @Test
+    fun coreTextField_interaction_dragged_vertically_cancelled() {
+        val state = mutableStateOf(TextFieldValue("test\n".repeat(10)))
+        val interactionState = InteractionState()
+        rule.setContent {
+            BasicTextField(
+                modifier = Modifier.size(50.dp).testTag(testTag),
+                value = state.value,
+                maxLines = 3,
+                onValueChange = { state.value = it },
+                interactionState = interactionState
+            )
+        }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                down(center)
+                moveBy(Offset(x = 0f, y = 150f))
+            }
+        assertThat(interactionState.value).contains(Interaction.Dragged)
+        rule.onNodeWithTag(testTag)
+            .performGesture {
+                cancel()
+            }
+        assertThat(interactionState.value).doesNotContain(Interaction.Dragged)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutDirectionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutDirectionTest.kt
new file mode 100644
index 0000000..e7196f5
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutDirectionTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.runtime.Providers
+import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class TextLayoutDirectionTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun testCoreTextField_getsCorrectLayoutDirection() {
+        var layoutDirection: LayoutDirection? = null
+
+        rule.setContent {
+            Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                @OptIn(InternalTextApi::class)
+                CoreTextField(
+                    value = TextFieldValue("..."),
+                    onValueChange = {},
+                    onTextLayout = { result ->
+                        layoutDirection = result.layoutInput.layoutDirection
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(layoutDirection).isNotNull()
+            assertThat(layoutDirection!!).isEqualTo(LayoutDirection.Rtl)
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
index 036f2c4..c338a45 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextDelegate
@@ -60,7 +61,10 @@
     style = FontStyle.Normal
 )
 
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class MultiWidgetSelectionDelegateTest {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index a5f7e13..8297a04 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -43,7 +43,10 @@
 import androidx.compose.ui.platform.AmbientView
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.selection.SelectionContainer
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.TextStyle
@@ -59,6 +62,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.width
 import androidx.test.espresso.matcher.BoundedMatcher
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -396,6 +400,8 @@
     }
 }
 
+private fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
+
 private class PointerInputChangeLog : (PointerEvent, PointerEventPass) -> Unit {
 
     val entries = mutableListOf<PointerInputChangeLogEntry>()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index dd838db..119d458 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
@@ -17,12 +17,8 @@
 package androidx.compose.foundation.text.selection
 
 import android.view.View
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.window.isPopupLayout
 import androidx.test.espresso.Espresso
 import androidx.test.espresso.Root
@@ -38,7 +34,7 @@
 ) {
     // Make sure that current measurement/drawing is finished
     runOnIdle { }
-    Espresso.onView(CoreMatchers.instanceOf(Owner::class.java))
+    Espresso.onView(CoreMatchers.instanceOf(ViewRootForTest::class.java))
         .inRoot(DoubleSelectionHandleMatcher(index))
         .check(ViewAssertions.matches(viewMatcher))
 }
@@ -58,15 +54,3 @@
         return matches && popupsMatchedSoFar == index + 1
     }
 }
-
-internal fun ComposeTestRule.rootWidth(): Dp {
-    val nodeInteraction = onRoot()
-    val node = nodeInteraction.fetchSemanticsNode("Failed to get screen width")
-
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-
-    return with(owner.density) {
-        owner.view.width.toDp()
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png b/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png
new file mode 100644
index 0000000..4eb583c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png
Binary files differ
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
similarity index 66%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
index 3bc2684..13c4896 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
 <!--
   Copyright 2020 The Android Open Source Project
 
@@ -14,6 +13,13 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.car.app">
-</manifest>
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:fillColor="#FF0000"
+        android:pathData="L24,0,24,24,0,24z"/>
+</vector>
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt
index bb7cbcf..6bb3c64 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt
@@ -123,7 +123,7 @@
         fun velocity(time: Long): Float {
             val splinePos = if (duration > 0) time / duration.toFloat() else 1f
             return AndroidFlingSpline.flingPosition(splinePos).velocityCoefficient *
-                distance / duration * 1000.0f
+                sign(initialVelocity) * distance / duration * 1000.0f
         }
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 31b3a08..9235b86 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -16,7 +16,7 @@
 
 package androidx.compose.foundation
 
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Size
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 627923d..fa6037b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -25,9 +25,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.semantics
@@ -43,7 +42,6 @@
  * @param interactionState [InteractionState] that will be updated to contain [Interaction.Focused]
  * when this focusable is focused
  */
-@OptIn(ExperimentalFocus::class)
 fun Modifier.focusable(
     enabled: Boolean = true,
     interactionState: InteractionState? = null,
@@ -69,7 +67,7 @@
             .semantics {
                 this.focused = isFocused
             }
-            .focusObserver {
+            .onFocusChanged {
                 isFocused = it.isFocused
                 if (isFocused) {
                     interactionState?.addInteraction(Interaction.Focused)
@@ -81,4 +79,4 @@
     } else {
         Modifier
     }
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
index aa0dd3f..6b94200 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
@@ -21,7 +21,7 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt
index b5b0c00..409e25d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.AccessibilityRangeInfo
-import androidx.compose.ui.semantics.accessibilityValue
-import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.stateDescriptionRange
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.util.annotation.FloatRange
 import androidx.compose.ui.util.format
@@ -52,8 +52,8 @@
     }
 
     return semantics {
-        accessibilityValue = Strings.TemplatePercent.format(percent)
-        accessibilityValueRange = AccessibilityRangeInfo(progress, 0f..1f)
+        stateDescription = Strings.TemplatePercent.format(percent)
+        stateDescriptionRange = AccessibilityRangeInfo(progress, 0f..1f)
     }
 }
 
@@ -69,5 +69,5 @@
  */
 @Stable
 fun Modifier.progressSemantics(): Modifier {
-    return semantics { accessibilityValue = Strings.InProgress }
+    return semantics { stateDescription = Strings.InProgress }
 }
\ No newline at end of file
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 04594e9..a40ca51 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
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.animation.FlingConfig
 import androidx.compose.foundation.animation.defaultFlingConfig
 import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.Scrollable
 import androidx.compose.foundation.gestures.ScrollableController
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Arrangement
@@ -122,7 +123,7 @@
     internal val flingConfig: FlingConfig,
     animationClock: AnimationClockObservable,
     interactionState: InteractionState? = null
-) {
+) : Scrollable {
 
     /**
      * current scroll position value in pixels
@@ -170,8 +171,7 @@
      *
      * If [scroll] is called from elsewhere, this will be canceled.
      */
-    @OptIn(ExperimentalFoundationApi::class)
-    suspend fun scroll(
+    override suspend fun scroll(
         block: suspend ScrollScope.() -> Unit
     ): Unit = scrollableController.scroll(block)
 
@@ -289,7 +289,6 @@
                 isScrollEnabled,
                 reverseScrolling = reverseScrollDirection
             )
-            .clipToBounds()
             .padding(contentPadding),
         verticalArrangement = verticalArrangement,
         horizontalAlignment = horizontalAlignment,
@@ -334,7 +333,6 @@
                 isScrollEnabled,
                 reverseScrolling = reverseScrollDirection
             )
-            .clipToBounds()
             .padding(contentPadding),
         horizontalArrangement = horizontalArrangement,
         verticalAlignment = verticalAlignment,
@@ -471,7 +469,7 @@
             val absScroll = if (isReversed) scroll - side else -scroll
             val xOffset = if (isVertical) 0 else absScroll.roundToInt()
             val yOffset = if (isVertical) absScroll.roundToInt() else 0
-            placeable.placeRelative(xOffset, yOffset)
+            placeable.placeRelativeWithLayer(xOffset, yOffset)
         }
     }
 }
@@ -480,7 +478,7 @@
     if (isVertical) {
         check(maxHeight != Constraints.Infinity) {
             "Nesting scrollable in the same direction layouts like ScrollableContainer and " +
-                "LazyColumnFor is not allowed. If you want to add a header before the list of" +
+                "LazyColumn is not allowed. If you want to add a header before the list of" +
                 " items please take a look on LazyColumn component which has a DSL api which" +
                 " allows to first add a header via item() function and then the list of " +
                 "items via items()."
@@ -488,7 +486,7 @@
     } else {
         check(maxWidth != Constraints.Infinity) {
             "Nesting scrollable in the same direction layouts like ScrollableRow and " +
-                "LazyRowFor is not allowed. If you want to add a header before the list of " +
+                "LazyRow is not allowed. If you want to add a header before the list of " +
                 "items please take a look on LazyRow component which has a DSL api which " +
                 "allows to first add a fixed element via item() function and then the " +
                 "list of items via items()."
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/animation/SmoothScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/animation/SmoothScroll.kt
new file mode 100644
index 0000000..feb7832
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/animation/SmoothScroll.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.animation
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.gestures.Scrollable
+import androidx.compose.runtime.dispatch.withFrameMillis
+
+/**
+ * Smooth scroll by [value] pixels.
+ *
+ * Cancels the currently running scroll, if any, and suspends until the cancellation is
+ * complete.
+ *
+ * @param value delta to scroll by
+ * @param spec [AnimationSpec] to be used for this smooth scrolling
+ *
+ * @return the amount of scroll consumed
+ */
+suspend fun Scrollable.smoothScrollBy(
+    value: Float,
+    spec: AnimationSpec<Float> = spring()
+): Float {
+    val animSpec = spec.vectorize(Float.VectorConverter)
+    val conv = Float.VectorConverter
+    val zeroVector = conv.convertToVector(0f)
+    val targetVector = conv.convertToVector(value)
+    var previousValue = 0f
+
+    scroll {
+        val startTimeMillis = withFrameMillis { it }
+        do {
+            val finished = withFrameMillis { frameTimeMillis ->
+                val newValue = conv.convertFromVector(
+                    animSpec.getValue(
+                        playTime = frameTimeMillis - startTimeMillis,
+                        start = zeroVector,
+                        end = targetVector,
+                        // TODO: figure out if/how we should incorporate existing velocity
+                        startVelocity = zeroVector
+                    )
+                )
+                val delta = newValue - previousValue
+                val consumed = scrollBy(delta)
+
+                if (consumed != delta) {
+                    previousValue += consumed
+                    true
+                } else {
+                    previousValue = newValue
+                    previousValue == value
+                }
+            }
+        } while (!finished)
+    }
+    return previousValue
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 25d8998..a131f9b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -17,15 +17,14 @@
 package androidx.compose.foundation.gestures
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
-import androidx.compose.ui.input.pointer.HandlePointerInputScope
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.anyPositionChangeConsumed
 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.consumePositionChange
 import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
 import androidx.compose.ui.input.pointer.positionChangedIgnoreConsumed
@@ -36,7 +35,8 @@
 /**
  * Waits for drag motion to pass [touch slop][ViewConfiguration.touchSlop], using [pointerId] as
  * the pointer to examine. If [pointerId] is raised, another pointer from those that are down
- * will be chosen to lead the gesture, and if none are down, `null` is returned.
+ * will be chosen to lead the gesture, and if none are down, `null` is returned. If [pointerId]
+ * is not down when [awaitTouchSlopOrCancellation] is called, then `null` is returned.
 
  * [onTouchSlopReached] is called after [ViewConfiguration.touchSlop] motion in the any direction
  * with the change that caused the motion beyond touch slop and the [Offset] beyond touch slop that
@@ -54,11 +54,13 @@
  * @see awaitHorizontalTouchSlopOrCancellation
  * @see awaitVerticalTouchSlopOrCancellation
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.awaitTouchSlopOrCancellation(
+suspend fun AwaitPointerEventScope.awaitTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
 ): PointerInputChange? {
+    if (currentEvent.isPointerUp(pointerId)) {
+        return null // The pointer has already been lifted, so the gesture is canceled
+    }
     var offset = Offset.Zero
     val touchSlop = viewConfiguration.touchSlop
 
@@ -119,8 +121,7 @@
  * @see horizontalDrag
  * @see verticalDrag
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.drag(
+suspend fun AwaitPointerEventScope.drag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit
 ): Boolean {
@@ -144,7 +145,8 @@
  * that is down will be used, if available, so the returned [PointerInputChange.id] may
  * differ from [pointerId]. If the position change in the any direction has been
  * consumed by the [PointerEventPass.Main] pass, then the drag is considered canceled and `null`
- * is returned.
+ * is returned.  If [pointerId] is not down when [awaitDragOrCancellation] is called, then
+ * `null` is returned.
  *
  * Example Usage:
  * @sample androidx.compose.foundation.samples.AwaitDragOrCancellationSample
@@ -153,10 +155,12 @@
  * @see awaitHorizontalDragOrCancellation
  * @see drag
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.awaitDragOrCancellation(
+suspend fun AwaitPointerEventScope.awaitDragOrCancellation(
     pointerId: PointerId,
 ): PointerInputChange? {
+    if (currentEvent.isPointerUp(pointerId)) {
+        return null // The pointer has already been lifted, so the gesture is canceled
+    }
     val change = awaitDragOrUp(pointerId) { it.positionChangedIgnoreConsumed() }
     return if (change.anyPositionChangeConsumed()) null else change
 }
@@ -175,14 +179,13 @@
  * @see detectVerticalDragGestures
  * @see detectHorizontalDragGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectDragGestures(
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
     onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
 ) {
     forEachGesture {
-        handlePointerInput {
+        awaitPointerEventScope {
             val down = awaitFirstDown()
             var drag: PointerInputChange?
             do {
@@ -207,7 +210,9 @@
  * Waits for vertical drag motion to pass [touch slop][ViewConfiguration.touchSlop], using
  * [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from
  * those that are down will be chosen to lead the gesture, and if none are down, `null` is returned.
-
+ * If [pointerId] is not down when [awaitVerticalTouchSlopOrCancellation] is called, then `null`
+ * is returned.
+ *
  * [onTouchSlopReached] is called after [ViewConfiguration.touchSlop] motion in the vertical
  * direction with the change that caused the motion beyond touch slop and the pixels beyond touch
  * slop. [onTouchSlopReached] should consume the position change if it accepts the motion.
@@ -224,18 +229,13 @@
  * @see awaitHorizontalTouchSlopOrCancellation
  * @see awaitTouchSlopOrCancellation
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.awaitVerticalTouchSlopOrCancellation(
+suspend fun AwaitPointerEventScope.awaitVerticalTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
 ) = awaitTouchSlopOrCancellation(
     pointerId = pointerId,
     onTouchSlopReached = onTouchSlopReached,
-    getDragDirectionValue = { it.y },
-    consumeMotion = { change, consumed ->
-        change.consumePositionChange(0f, consumed)
-    },
-    getCrossDirectionValue = { it.x }
+    getDragDirectionValue = { it.y }
 )
 
 /**
@@ -254,8 +254,7 @@
  * @see horizontalDrag
  * @see drag
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.verticalDrag(
+suspend fun AwaitPointerEventScope.verticalDrag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit
 ): Boolean = drag(
@@ -272,7 +271,8 @@
  * that is down will be used, if available, so the returned [PointerInputChange.id] may
  * differ from [pointerId]. If the position change in the vertical direction has been
  * consumed by the [PointerEventPass.Main] pass, then the drag is considered canceled and `null` is
- * returned.
+ * returned. If [pointerId] is not down when [awaitVerticalDragOrCancellation] is called, then
+ * `null` is returned.
  *
  * Example Usage:
  * @sample androidx.compose.foundation.samples.AwaitVerticalDragOrCancellationSample
@@ -281,10 +281,12 @@
  * @see awaitDragOrCancellation
  * @see verticalDrag
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.awaitVerticalDragOrCancellation(
+suspend fun AwaitPointerEventScope.awaitVerticalDragOrCancellation(
     pointerId: PointerId,
 ): PointerInputChange? {
+    if (currentEvent.isPointerUp(pointerId)) {
+        return null // The pointer has already been lifted, so the gesture is canceled
+    }
     val change = awaitDragOrUp(pointerId) { it.positionChangeIgnoreConsumed().y != 0f }
     return if (change.consumed.positionChange.y != 0f) null else change
 }
@@ -307,14 +309,13 @@
  * @see detectDragGestures
  * @see detectHorizontalDragGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectVerticalDragGestures(
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
     onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
 ) {
     forEachGesture {
-        handlePointerInput {
+        awaitPointerEventScope {
             val down = awaitFirstDown()
             val drag = awaitVerticalTouchSlopOrCancellation(down.id, onVerticalDrag)
             if (drag != null) {
@@ -341,7 +342,8 @@
  * direction with the change that caused the motion beyond touch slop and the pixels beyond touch
  * slop. [onTouchSlopReached] should consume the position change if it accepts the motion.
  * If it does, then the method returns that [PointerInputChange]. If not, touch slop detection will
- * continue.
+ * continue. If [pointerId] is not down when [awaitHorizontalTouchSlopOrCancellation] is called,
+ * then `null` is returned.
  *
  * @return The [PointerInputChange] that was consumed in [onTouchSlopReached] or `null` if all
  * pointers are raised before touch slop is detected or another gesture consumed the position
@@ -353,18 +355,13 @@
  * @see awaitVerticalTouchSlopOrCancellation
  * @see awaitTouchSlopOrCancellation
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.awaitHorizontalTouchSlopOrCancellation(
+suspend fun AwaitPointerEventScope.awaitHorizontalTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
 ) = awaitTouchSlopOrCancellation(
     pointerId = pointerId,
     onTouchSlopReached = onTouchSlopReached,
-    getDragDirectionValue = { it.x },
-    consumeMotion = { change, consumed ->
-        change.consumePositionChange(consumed, 0f)
-    },
-    getCrossDirectionValue = { it.y }
+    getDragDirectionValue = { it.x }
 )
 
 /**
@@ -380,8 +377,7 @@
  * @see verticalDrag
  * @see drag
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.horizontalDrag(
+suspend fun AwaitPointerEventScope.horizontalDrag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit
 ): Boolean = drag(
@@ -398,7 +394,8 @@
  * that is down will be used, if available, so the returned [PointerInputChange.id] may
  * differ from [pointerId]. If the position change in the horizontal direction has been
  * consumed by the [PointerEventPass.Main] pass, then the drag is considered canceled and `null`
- * is returned.
+ * is returned. If [pointerId] is not down when [awaitHorizontalDragOrCancellation] is called,
+ * then `null` is returned.
  *
  * Example Usage:
  * @sample androidx.compose.foundation.samples.AwaitHorizontalDragOrCancellationSample
@@ -407,10 +404,12 @@
  * @see awaitVerticalDragOrCancellation
  * @see awaitDragOrCancellation
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.awaitHorizontalDragOrCancellation(
+suspend fun AwaitPointerEventScope.awaitHorizontalDragOrCancellation(
     pointerId: PointerId,
 ): PointerInputChange? {
+    if (currentEvent.isPointerUp(pointerId)) {
+        return null // The pointer has already been lifted, so the gesture is canceled
+    }
     val change = awaitDragOrUp(pointerId) { it.positionChangeIgnoreConsumed().x != 0f }
     return if (change.consumed.positionChange.x != 0f) null else change
 }
@@ -433,14 +432,13 @@
  * @see detectVerticalDragGestures
  * @see detectDragGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectHorizontalDragGestures(
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
     onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
 ) {
     forEachGesture {
-        handlePointerInput {
+        awaitPointerEventScope {
             val down = awaitFirstDown()
             val drag = awaitHorizontalTouchSlopOrCancellation(down.id, onHorizontalDrag)
             if (drag != null) {
@@ -468,13 +466,15 @@
  * @return `true` when the gesture ended with all pointers up and `false` when the gesture
  * was canceled.
  */
-@ExperimentalPointerInput
-private suspend inline fun HandlePointerInputScope.drag(
+private suspend inline fun AwaitPointerEventScope.drag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit,
     motionFromChange: (PointerInputChange) -> Float,
     motionConsumed: (PointerInputChange) -> Boolean
 ): Boolean {
+    if (currentEvent.isPointerUp(pointerId)) {
+        return false // The pointer has already been lifted, so the gesture is canceled
+    }
     var pointer = pointerId
     while (true) {
         val change = awaitDragOrUp(pointer) { motionFromChange(it) != 0f }
@@ -499,8 +499,7 @@
  * returned. When a drag is detected, that [PointerInputChange] is returned. A drag is
  * only detected when [hasDragged] returns `true`.
  */
-@ExperimentalPointerInput
-private suspend inline fun HandlePointerInputScope.awaitDragOrUp(
+private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
     pointerId: PointerId,
     hasDragged: (PointerInputChange) -> Boolean
 ): PointerInputChange {
@@ -526,36 +525,33 @@
  * Waits for drag motion along one axis based on [getDragDirectionValue] to pass touch slop,
  * using [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer
  * from those that are down will be chosen to lead the gesture, and if none are down,
- * `null` is returned.
+ * `null` is returned. If [pointerId] is not down when [awaitTouchSlopOrCancellation] is called,
+ * then `null` is returned.
  *
  * When touch slop is detected, [onTouchSlopReached] is called with the change and the distance
  * beyond the touch slop. [getDragDirectionValue] should return the position change in the direction
- * of the drag and [getCrossDirectionValue] should return the position change along the
- * perpendicular axis. If [onTouchSlopReached] does not consume the position change, touch slop
+ * of the drag axis. If [onTouchSlopReached] does not consume the position change, touch slop
  * will not have been considered detected and the detection will continue or, if it is consumed,
  * the [PointerInputChange] that was consumed will be returned.
  *
  * This works with [awaitTouchSlopOrCancellation] for the other axis to ensure that only horizontal
  * or vertical dragging is done, but not both.
  *
- * [consumeMotion] should consume the position change along the drag axis.
- *
  * @return The [PointerInputChange] of the event that was consumed in [onTouchSlopReached] or
  * `null` if all pointers are raised or the position change was consumed by another gesture
  * detector.
  */
-@ExperimentalPointerInput
-private suspend inline fun HandlePointerInputScope.awaitTouchSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (PointerInputChange, Float) -> Unit,
-    getDragDirectionValue: (Offset) -> Float,
-    consumeMotion: (PointerInputChange, Float) -> Unit,
-    getCrossDirectionValue: (Offset) -> Float
+    getDragDirectionValue: (Offset) -> Float
 ): PointerInputChange? {
+    if (currentEvent.isPointerUp(pointerId)) {
+        return null // The pointer has already been lifted, so the gesture is canceled
+    }
     val touchSlop = viewConfiguration.touchSlop
     var pointer: PointerId = pointerId
     var totalPositionChange = 0f
-    var totalCrossPositionChange = 0f
 
     while (true) {
         val event = awaitPointerEvent()
@@ -576,11 +572,8 @@
             val positionChange = getDragDirectionValue(currentPosition) -
                 getDragDirectionValue(previousPosition)
             totalPositionChange += positionChange
-            totalCrossPositionChange +=
-                getCrossDirectionValue(currentPosition) - getCrossDirectionValue(previousPosition)
 
             val inDirection = abs(totalPositionChange)
-            val crossDirection = abs(totalCrossPositionChange)
             if (inDirection < touchSlop) {
                 // verify that nothing else consumed the drag event
                 awaitPointerEvent(PointerEventPass.Final)
@@ -588,22 +581,6 @@
                     return null
                 }
             } else {
-                if (crossDirection > inDirection) {
-                    // Consume the position change in the direction that we care about
-                    consumeMotion(dragEvent, positionChange)
-
-                    // give the other direction a chance to consume
-                    awaitPointerEvent(PointerEventPass.Final)
-
-                    // Unconsume the position change
-                    consumeMotion(dragEvent, -positionChange)
-
-                    if (dragEvent.anyPositionChangeConsumed()) {
-                        return null
-                    } else {
-                        totalCrossPositionChange = 0f
-                    }
-                }
                 onTouchSlopReached(
                     dragEvent,
                     totalPositionChange - (sign(totalPositionChange) * touchSlop)
@@ -617,3 +594,6 @@
         }
     }
 }
+
+private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
+    changes.firstOrNull { it.id == pointerId }?.current?.down != true
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
index b116305..1af4d78 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
@@ -15,8 +15,7 @@
  */
 package androidx.compose.foundation.gestures
 
-import androidx.compose.ui.gesture.ExperimentalPointerInput
-import androidx.compose.ui.input.pointer.HandlePointerInputScope
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.util.fastAny
@@ -37,7 +36,6 @@
  * exits if [isActive] is `false`.
  */
 @OptIn(InternalCoroutinesApi::class, ExperimentalStdlibApi::class)
-@ExperimentalPointerInput
 suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {
     while (isActive) {
         try {
@@ -59,22 +57,20 @@
  * Returns `true` if the current state of the pointer events has all pointers up and `false`
  * if any of the pointers are down.
  */
-@ExperimentalPointerInput
-internal fun HandlePointerInputScope.allPointersUp(): Boolean = !currentPointers.fastAny { it.down }
+internal fun AwaitPointerEventScope.allPointersUp(): Boolean =
+    !currentEvent.changes.fastAny { it.current.down }
 
 /**
  * Waits for all pointers to be up before returning.
  */
-@ExperimentalPointerInput
 internal suspend fun PointerInputScope.awaitAllPointersUp() {
-    handlePointerInput { awaitAllPointersUp() }
+    awaitPointerEventScope { awaitAllPointersUp() }
 }
 
 /**
  * Waits for all pointers to be up before returning.
  */
-@ExperimentalPointerInput
-internal suspend fun HandlePointerInputScope.awaitAllPointersUp() {
+internal suspend fun AwaitPointerEventScope.awaitAllPointersUp() {
     if (!allPointersUp()) {
         do {
             val events = awaitPointerEvent(PointerEventPass.Final)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt
index 0c4efef..45b94d4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation.gestures
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.PointerInputScope
@@ -32,9 +31,6 @@
 import kotlin.math.abs
 import kotlin.math.atan2
 
-private val NoRotateZoom: (Float) -> Unit = { }
-private val NoPan: (Offset) -> Unit = { }
-
 /**
  * A gesture detector for rotationg, panning, and zoom. Once touch slop has been reached, the
  * user can use rotation, panning and zoom gestures. [onRotate] will be called when rotation
@@ -51,15 +47,12 @@
  * Example Usage:
  * @sample androidx.compose.foundation.samples.DetectMultitouchGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectMultitouchGestures(
     panZoomLock: Boolean = false,
-    onRotate: (rotation: Float) -> Unit = NoRotateZoom,
-    onZoom: (zoom: Float) -> Unit = NoRotateZoom,
-    onPan: (pan: Offset) -> Unit = NoPan
+    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
 ) {
     forEachGesture {
-        handlePointerInput {
+        awaitPointerEventScope {
             var rotation = 0f
             var zoom = 1f
             var pan = Offset.Zero
@@ -94,21 +87,21 @@
                             lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
                         }
                     }
+
                     if (pastTouchSlop) {
+                        val centroid = event.calculateCentroid(useCurrent = false)
+                        val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
+                        if (effectiveRotation != 0f ||
+                            zoomChange != 1f ||
+                            panChange != Offset.Zero
+                        ) {
+                            onGesture(centroid, panChange, zoomChange, effectiveRotation)
+                        }
                         event.changes.fastForEach {
                             if (it.positionChanged()) {
                                 it.consumeAllChanges()
                             }
                         }
-                        if (!lockedToPanZoom && rotationChange != 0f) {
-                            onRotate(rotationChange)
-                        }
-                        if (zoomChange != 1f) {
-                            onZoom(zoomChange)
-                        }
-                        if (panChange != Offset.Zero) {
-                            onPan(panChange)
-                        }
                     }
                 }
             } while (!canceled && event.changes.fastAny { it.current.down })
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 830e5bc..481ecd2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -24,9 +24,6 @@
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.animation.FlingConfig
@@ -34,18 +31,25 @@
 import androidx.compose.foundation.animation.fling
 import androidx.compose.runtime.AtomicReference
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.dispatch.withFrameMillis
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.onDispose
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.Direction
 import androidx.compose.ui.gesture.ScrollCallback
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.minus
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.sync.Mutex
@@ -75,18 +79,6 @@
 }
 
 /**
- * Scope used for suspending scroll blocks
- */
-interface ScrollScope {
-    /**
-     * Attempts to scroll forward by [pixels] px.
-     *
-     * @return the amount of the requested scroll that was consumed (that is, how far it scrolled)
-     */
-    fun scrollBy(pixels: Float): Float
-}
-
-/**
  * Controller to control the [scrollable] modifier with. Contains necessary information about the
  * ongoing fling and provides smooth scrolling capabilities.
  *
@@ -104,7 +96,7 @@
     internal val flingConfig: FlingConfig,
     animationClock: AnimationClockObservable,
     internal val interactionState: InteractionState? = null
-) {
+) : Scrollable {
     /**
      * Smooth scroll by [value] amount of pixels
      *
@@ -121,57 +113,6 @@
         animatedFloat.animateTo(to, anim = spec, onEnd = onEnd)
     }
 
-    /**
-     * Smooth scroll by [value] pixels.
-     *
-     * Cancels the currently running scroll, if any, and suspends until the cancellation is
-     * complete.
-     *
-     * @param value delta to scroll by
-     * @param spec [AnimationSpec] to be used for this smooth scrolling
-     *
-     * @return the amount of scroll consumed
-     */
-    @OptIn(ExperimentalFoundationApi::class)
-    suspend fun smoothScrollBy(
-        value: Float,
-        spec: AnimationSpec<Float> = spring()
-    ): Float {
-        val animSpec = spec.vectorize(Float.VectorConverter)
-        val conv = Float.VectorConverter
-        val zeroVector = conv.convertToVector(0f)
-        val targetVector = conv.convertToVector(value)
-        var previousValue = 0f
-
-        scroll {
-            val startTimeMillis = withFrameMillis { it }
-            do {
-                val finished = withFrameMillis { frameTimeMillis ->
-                    val newValue = conv.convertFromVector(
-                        animSpec.getValue(
-                            playTime = frameTimeMillis - startTimeMillis,
-                            start = zeroVector,
-                            end = targetVector,
-                            // TODO: figure out if/how we should incorporate existing velocity
-                            startVelocity = zeroVector
-                        )
-                    )
-                    val delta = newValue - previousValue
-                    val consumed = scrollBy(delta)
-
-                    if (consumed != delta) {
-                        previousValue += consumed
-                        true
-                    } else {
-                        previousValue = newValue
-                        previousValue == value
-                    }
-                }
-            } while (!finished)
-        }
-        return previousValue
-    }
-
     private val scrollControlJob = AtomicReference<Job?>(null)
     private val scrollControlMutex = Mutex()
 
@@ -190,7 +131,7 @@
      *
      * If [scroll] is called from elsewhere, this will be canceled.
      */
-    suspend fun scroll(
+    override suspend fun scroll(
         block: suspend ScrollScope.() -> Unit
     ): Unit = coroutineScope {
         stopFlingAnimation()
@@ -258,24 +199,85 @@
     }
 
     private val animatedFloat =
-        DeltaAnimatedFloat(0f, clocksProxy, consumeScrollDelta)
+        DeltaAnimatedFloat(0f, clocksProxy) {
+            dispatchScroll(it.reverseIfNeeded(), NestedScrollSource.Fling)
+        }
 
-    /**
-     * current position for scrollable
-     */
-    internal var value: Float
-        get() = animatedFloat.value
-        set(value) = animatedFloat.snapTo(value)
+    private var orientation by mutableStateOf(Orientation.Vertical)
+    private var reverseDirection by mutableStateOf(false)
 
-    internal fun fling(velocity: Float, onScrollEnd: (Float) -> Unit) {
+    // this is not good, should be gone when we have sync (suspend) animation and scroll
+    internal fun update(orientation: Orientation, reverseDirection: Boolean) {
+        this.orientation = orientation
+        this.reverseDirection = reverseDirection
+    }
+
+    internal val nestedScrollDispatcher = NestedScrollDispatcher()
+
+    internal val nestedScrollConnection = object : NestedScrollConnection {
+        override fun onPostScroll(
+            consumed: Offset,
+            available: Offset,
+            source: NestedScrollSource
+        ): Offset = performDeltaConsumption(available)
+
+        override fun onPostFling(
+            consumed: Velocity,
+            available: Velocity,
+            onFinished: (Velocity) -> Unit
+        ) {
+            performFlingInternal(available.pixelsPerSecond) { leftAfterUs ->
+                onFinished.invoke(available - Velocity(leftAfterUs))
+            }
+        }
+    }
+
+    internal fun dispatchScroll(scrollDelta: Float, source: NestedScrollSource) {
+        val scrollOffset = scrollDelta.toOffset()
+        val preConsumedByParent = nestedScrollDispatcher.dispatchPreScroll(scrollOffset, source)
+
+        val scrollAvailable = scrollOffset - preConsumedByParent
+        val consumed = performDeltaConsumption(scrollAvailable)
+        val leftForParent = scrollAvailable - consumed
+        nestedScrollDispatcher.dispatchPostScroll(consumed, leftForParent, source)
+    }
+
+    private fun performDeltaConsumption(delta: Offset): Offset {
+        // reverse once for users if needed and then back to the original axis system
+        return consumeScrollDelta(delta.toFloat().reverseIfNeeded()).reverseIfNeeded().toOffset()
+    }
+
+    internal fun dispatchFling(velocity: Float, onScrollEnd: (Float) -> Unit) {
+        val consumedByParent =
+            nestedScrollDispatcher.dispatchPreFling(Velocity(velocity.toOffset()))
+        val available = velocity.toOffset() - consumedByParent.pixelsPerSecond
+        performFlingInternal(available) { velocityLeft ->
+            // when notifying users code -- reverse if needed to obey their setting
+            onScrollEnd(velocityLeft.toFloat().reverseIfNeeded())
+            nestedScrollDispatcher.dispatchPostFling(
+                Velocity(available - velocityLeft),
+                Velocity(velocityLeft)
+            )
+        }
+    }
+
+    private fun performFlingInternal(velocity: Offset, onScrollEnd: (Offset) -> Unit) {
         animatedFloat.fling(
             config = flingConfig,
-            startVelocity = velocity,
+            startVelocity = velocity.toFloat().reverseIfNeeded(),
             onAnimationEnd = { _, _, velocityLeft ->
-                onScrollEnd(velocityLeft)
+                onScrollEnd(velocityLeft.reverseIfNeeded().toOffset())
             }
         )
     }
+
+    private fun Float.toOffset(): Offset =
+        if (orientation == Orientation.Horizontal) Offset(this, 0f) else Offset(0f, this)
+
+    private fun Offset.toFloat(): Float =
+        if (orientation == Orientation.Horizontal) this.x else this.y
+
+    private fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this
 }
 
 /**
@@ -315,6 +317,7 @@
     onScrollStopped: (velocity: Float) -> Unit = {}
 ): Modifier = composed(
     factory = {
+        controller.update(orientation, reverseDirection)
         onDispose {
             controller.stopAnimation()
             controller.interactionState?.removeInteraction(Interaction.Dragged)
@@ -333,10 +336,9 @@
             override fun onScroll(scrollDistance: Float): Float {
                 if (!enabled) return 0f
                 controller.stopFlingAnimation()
-                val toConsume = if (reverseDirection) scrollDistance * -1 else scrollDistance
-                val consumed = controller.consumeScrollDelta(toConsume)
-                controller.value = controller.value + consumed
-                return if (reverseDirection) consumed * -1 else consumed
+                controller.dispatchScroll(scrollDistance, NestedScrollSource.Drag)
+                // consume everything since we handle nested scrolling separately
+                return scrollDistance
             }
 
             override fun onCancel() {
@@ -349,8 +351,8 @@
             override fun onStop(velocity: Float) {
                 controller.interactionState?.removeInteraction(Interaction.Dragged)
                 if (enabled) {
-                    controller.fling(
-                        velocity = if (reverseDirection) velocity * -1 else velocity,
+                    controller.dispatchFling(
+                        velocity = velocity,
                         onScrollEnd = onScrollStopped
                     )
                 }
@@ -365,7 +367,7 @@
         ).mouseScrollable(
             scrollCallback,
             orientation
-        )
+        ).nestedScroll(controller.nestedScrollConnection, controller.nestedScrollDispatcher)
     },
     inspectorInfo = debugInspectorInfo {
         name = "scrollable"
@@ -398,7 +400,7 @@
 private class DeltaAnimatedFloat(
     initial: Float,
     clock: AnimationClockObservable,
-    private val onDelta: (Float) -> Float
+    private val onDelta: (Float) -> Unit
 ) : AnimatedFloat(clock, Spring.DefaultDisplacementThreshold) {
 
     override var value = initial
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ScrollableInterface.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ScrollableInterface.kt
new file mode 100644
index 0000000..aaa5e85
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ScrollableInterface.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gestures
+
+import androidx.compose.foundation.ScrollableColumn
+// ktlint doesn't detect this being used in a doc comment
+import androidx.compose.foundation.animation.smoothScrollBy // ktlint-disable
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+
+/**
+ * Scope used for suspending scroll blocks
+ */
+interface ScrollScope {
+    /**
+     * Attempts to scroll forward by [pixels] px.
+     *
+     * @return the amount of the requested scroll that was consumed (that is, how far it scrolled)
+     */
+    fun scrollBy(pixels: Float): Float
+}
+
+/**
+ * A an object representing something that can be scrolled. This interface is implemented by states
+ * of scrollable containers such as [ScrollableColumn] and [LazyColumn] in order to provide
+ * low-level scrolling control via [scroll], as well as allowing for higher-level scrolling
+ * functions like  [Scrollable.smoothScrollBy] to be implemented as extension functions on
+ * [Scrollable].
+ *
+ * Subclasses may also have their own methods that are specific to their interaction paradigm, such
+ * as [LazyListState.snapToItemIndex].
+ *
+ * @see ScrollableController
+ * @see Scrollable.smoothScrollBy
+ */
+interface Scrollable {
+    /**
+     * 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
+     * performed within a [scroll] block (even if they don't call any other methods on this
+     * object) in order to guarantee that mutual exclusion is enforced.
+     *
+     * Cancels the currently running scroll, if any, and suspends until the cancellation is
+     * complete.
+     *
+     * If [scroll] is called from elsewhere, this will be canceled.
+     */
+    suspend fun scroll(
+        block: suspend ScrollScope.() -> Unit
+    )
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index d9f141b..3f97af9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -17,8 +17,7 @@
 package androidx.compose.foundation.gestures
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
-import androidx.compose.ui.input.pointer.HandlePointerInputScope
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputChange
@@ -86,7 +85,6 @@
  * the gestures are considered canceled. [onDoubleTap], [onLongPress], and [onTap] will not be
  * called after a gesture has been canceled.
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectTapGestures(
     onDoubleTap: (() -> Unit)? = null,
     onLongPress: (() -> Unit)? = null,
@@ -97,7 +95,7 @@
     forEachGesture {
         coroutineScope {
             pressScope.reset()
-            val down = handlePointerInput {
+            val down = awaitPointerEventScope {
                 awaitFirstDown().also {
                     it.consumeDownChange()
                 }
@@ -117,7 +115,7 @@
             try {
                 // wait for first tap up or long press
                 up = withTimeout(longPressTimeout.inMilliseconds()) {
-                    handlePointerInput {
+                    awaitPointerEventScope {
                         waitForUpOrCancellation()?.also { it.consumeDownChange() }
                     }
                 }
@@ -153,7 +151,7 @@
                         try {
                             // Might have a long second press as the second tap
                             withTimeout(longPressTimeout.inMilliseconds()) {
-                                handlePointerInput {
+                                awaitPointerEventScope {
                                     val secondUp = waitForUpOrCancellation()
                                     if (secondUp == null) {
                                         pressScope.cancel()
@@ -186,8 +184,7 @@
  * Reads events until the first down is received. If [requireUnconsumed] is `true` and the first
  * down is consumed in the [PointerEventPass.Main] pass, that gesture is ignored.
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.awaitFirstDown(
+suspend fun AwaitPointerEventScope.awaitFirstDown(
     requireUnconsumed: Boolean = true
 ): PointerInputChange {
     var event: PointerEvent
@@ -208,8 +205,7 @@
  * pass. If the gesture was not canceled, the final up change is returned or `null` if the
  * event was canceled.
  */
-@ExperimentalPointerInput
-suspend fun HandlePointerInputScope.waitForUpOrCancellation(): PointerInputChange? {
+suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange? {
     while (true) {
         val event = awaitPointerEvent(PointerEventPass.Main)
         if (event.changes.fastAll { it.changedToUp() }) {
@@ -233,9 +229,8 @@
 /**
  * Consumes all event changes in the [PointerEventPass.Initial] until all pointers are up.
  */
-@ExperimentalPointerInput
 private suspend fun PointerInputScope.consumeAllEventsUntilUp() {
-    handlePointerInput {
+    awaitPointerEventScope {
         if (!allPointersUp()) {
             do {
                 val event = awaitPointerEvent(PointerEventPass.Initial)
@@ -251,12 +246,11 @@
  * not detected within [ViewConfiguration.doubleTapTimeout] of [upTime], `null` is returned.
  * Otherwise, the down event is returned.
  */
-@ExperimentalPointerInput
 private suspend fun PointerInputScope.detectSecondTapDown(
     upTime: Uptime
 ): PointerInputChange? {
     return withTimeoutOrNull(viewConfiguration.doubleTapTimeout.inMilliseconds()) {
-        handlePointerInput {
+        awaitPointerEventScope {
             val minUptime = upTime + viewConfiguration.doubleTapMinTime
             var change: PointerInputChange
             // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt
new file mode 100644
index 0000000..9c34dd5
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+/**
+ * Represents an index in the list of items of lazy list.
+ */
+@Suppress("NOTHING_TO_INLINE", "EXPERIMENTAL_FEATURE_WARNING")
+internal inline class DataIndex(val value: Int) {
+    inline operator fun inc(): DataIndex = DataIndex(value + 1)
+    inline operator fun dec(): DataIndex = DataIndex(value - 1)
+    inline operator fun plus(i: Int): DataIndex = DataIndex(value + i)
+    inline operator fun minus(i: Int): DataIndex = DataIndex(value - i)
+    inline operator fun minus(i: DataIndex): DataIndex = DataIndex(value - i.value)
+    inline operator fun compareTo(other: DataIndex): Int = value - other.value
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/IntervalList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/IntervalList.kt
new file mode 100644
index 0000000..1a61ac0
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/IntervalList.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+internal class IntervalHolder<T>(
+    val startIndex: Int,
+    val size: Int,
+    val content: T
+)
+
+internal class IntervalList<T> {
+    private val intervals = mutableListOf<IntervalHolder<T>>()
+    internal var totalSize = 0
+        private set
+
+    fun add(size: Int, content: T) {
+        if (size == 0) {
+            return
+        }
+
+        val interval = IntervalHolder(
+            startIndex = totalSize,
+            size = size,
+            content = content
+        )
+        totalSize += size
+        intervals.add(interval)
+    }
+
+    fun intervalForIndex(index: Int) =
+        if (index < 0 || index >= totalSize) {
+            throw IndexOutOfBoundsException("Index $index, size $totalSize")
+        } else {
+            intervals[findIndexOfHighestValueLesserThan(intervals, index)]
+        }
+
+    /**
+     * Finds the index of the [list] which contains the highest value of [IntervalHolder.startIndex]
+     * that is less than or equal to the given [value].
+     */
+    private fun findIndexOfHighestValueLesserThan(list: List<IntervalHolder<T>>, value: Int): Int {
+        var left = 0
+        var right = list.lastIndex
+
+        while (left < right) {
+            val middle = (left + right) / 2
+
+            val middleValue = list[middle].startIndex
+            if (middleValue == value) {
+                return middle
+            }
+
+            if (middleValue < value) {
+                left = middle + 1
+
+                // Verify that the left will not be bigger than our value
+                if (value < list[left].startIndex) {
+                    return middle
+                }
+            } else {
+                right = middle - 1
+            }
+        }
+
+        return left
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
index e3ebd07..7535b73 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
@@ -56,19 +58,13 @@
     )
 }
 
-internal class IntervalHolder(
-    val startIndex: Int,
-    val content: LazyItemScope.(Int) -> (@Composable () -> Unit)
-)
-
 internal class LazyListScopeImpl : LazyListScope {
-    private val intervals = mutableListOf<IntervalHolder>()
-    var totalSize = 0
+    private val intervals = IntervalList<LazyItemScope.(Int) -> (@Composable () -> Unit)>()
+
+    val totalSize get() = intervals.totalSize
 
     fun contentFor(index: Int, scope: LazyItemScope): @Composable () -> Unit {
-        val intervalIndex = findIndexOfHighestValueLesserThan(intervals, index)
-
-        val interval = intervals[intervalIndex]
+        val interval = intervals.intervalForIndex(index)
         val localIntervalIndex = index - interval.startIndex
 
         return interval.content(scope, localIntervalIndex)
@@ -78,84 +74,24 @@
         items: List<T>,
         itemContent: @Composable LazyItemScope.(item: T) -> Unit
     ) {
-        // There aren't any items to display
-        if (items.isEmpty()) { return }
-
-        val interval = IntervalHolder(
-            startIndex = totalSize,
-            content = { index ->
-                val item = items[index]
-
-                { itemContent(item) }
-            }
-        )
-
-        totalSize += items.size
-
-        intervals.add(interval)
+        intervals.add(items.size) { index ->
+            val item = items[index]
+            @Composable { itemContent(item) }
+        }
     }
 
     override fun item(content: @Composable LazyItemScope.() -> Unit) {
-        val interval = IntervalHolder(
-            startIndex = totalSize,
-            content = { { content() } }
-        )
-
-        totalSize += 1
-
-        intervals.add(interval)
+        intervals.add(1) { @Composable { content() } }
     }
 
     override fun <T> itemsIndexed(
         items: List<T>,
         itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
     ) {
-        // There aren't any items to display
-        if (items.isEmpty()) { return }
-
-        val interval = IntervalHolder(
-            startIndex = totalSize,
-            content = { index ->
-                val item = items[index]
-
-                { itemContent(index, item) }
-            }
-        )
-
-        totalSize += items.size
-
-        intervals.add(interval)
-    }
-
-    /**
-     * Finds the index of the [list] which contains the highest value of [IntervalHolder.startIndex]
-     * that is less than or equal to the given [value].
-     */
-    private fun findIndexOfHighestValueLesserThan(list: List<IntervalHolder>, value: Int): Int {
-        var left = 0
-        var right = list.lastIndex
-
-        while (left < right) {
-            val middle = (left + right) / 2
-
-            val middleValue = list[middle].startIndex
-            if (middleValue == value) {
-                return middle
-            }
-
-            if (middleValue < value) {
-                left = middle + 1
-
-                // Verify that the left will not be bigger than our value
-                if (value < list[left].startIndex) {
-                    return middle
-                }
-            } else {
-                right = middle - 1
-            }
+        intervals.add(items.size) { index ->
+            val item = items[index]
+            @Composable { itemContent(index, item) }
         }
-
-        return left
     }
 }
 
@@ -170,17 +106,28 @@
  * @param modifier the modifier to apply to this layout
  * @param state the state object to be used to control or observe the list's state
  * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
  * @param verticalAlignment the vertical alignment applied to the items
  * @param content a block which describes the content. Inside this block you can use methods like
  * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
  */
+@OptIn(InternalLayoutApi::class)
 @Composable
 fun LazyRow(
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal =
+        if (!reverseLayout) Arrangement.Start else Arrangement.End,
     verticalAlignment: Alignment.Vertical = Alignment.Top,
     content: LazyListScope.() -> Unit
 ) {
@@ -193,7 +140,9 @@
         state = state,
         contentPadding = contentPadding,
         verticalAlignment = verticalAlignment,
-        isVertical = false
+        horizontalArrangement = horizontalArrangement,
+        isVertical = false,
+        reverseLayout = reverseLayout
     ) { index ->
         scope.contentFor(index, this)
     }
@@ -207,20 +156,31 @@
  *
  * @sample androidx.compose.foundation.samples.LazyColumnSample
  *
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
- * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
- * @param horizontalAlignment the horizontal alignment applied to the items
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * we scrolled to the bottom.
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param horizontalAlignment the horizontal alignment applied to the items.
  * @param content a block which describes the content. Inside this block you can use methods like
  * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
  */
+@OptIn(InternalLayoutApi::class)
 @Composable
 fun LazyColumn(
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
     horizontalAlignment: Alignment.Horizontal = Alignment.Start,
     content: LazyListScope.() -> Unit
 ) {
@@ -233,7 +193,9 @@
         state = state,
         contentPadding = contentPadding,
         horizontalAlignment = horizontalAlignment,
-        isVertical = true
+        verticalArrangement = verticalArrangement,
+        isVertical = true,
+        reverseLayout = reverseLayout
     ) { index ->
         scope.contentFor(index, this)
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt
index 2031273..89b9186 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.runtime.Composable
@@ -29,14 +31,19 @@
  * See [LazyColumnForIndexed] if you need to have both item and index params in [itemContent].
  * See [LazyRowFor] if you are looking for a horizontally scrolling version.
  *
- * @sample androidx.compose.foundation.samples.LazyColumnForSample
- *
  * @param items the backing list of data to display
  * @param modifier the modifier to apply to this layout
  * @param state the state object to be used to control or observe the list's state
- * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * we scrolled to the bottom.
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
  * @param horizontalAlignment the horizontal alignment applied to the items
  * @param itemContent emits the UI for an item from [items] list. May emit any number of components,
  * which will be stacked vertically. Note that [LazyColumnFor] can start scrolling incorrectly
@@ -44,12 +51,24 @@
  * content asynchronously please reserve some space for the item, for example using [Spacer].
  * Use [LazyColumnForIndexed] if you need to have both index and item params.
  */
+@OptIn(InternalLayoutApi::class)
 @Composable
+@Deprecated(
+    "Use LazyColumn instead",
+    ReplaceWith(
+        "LazyColumn(modifier, state, contentPadding, horizontalAlignment = " +
+            "horizontalAlignment) { \n items(items, itemContent) \n }",
+        "androidx.compose.foundation.lazy.LazyColumn"
+    )
+)
 fun <T> LazyColumnFor(
     items: List<T>,
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
     horizontalAlignment: Alignment.Horizontal = Alignment.Start,
     itemContent: @Composable LazyItemScope.(T) -> Unit
 ) {
@@ -57,7 +76,9 @@
         modifier = modifier,
         state = state,
         contentPadding = contentPadding,
-        horizontalAlignment = horizontalAlignment
+        horizontalAlignment = horizontalAlignment,
+        verticalArrangement = verticalArrangement,
+        reverseLayout = reverseLayout
     ) {
         items(items, itemContent)
     }
@@ -71,14 +92,19 @@
  *
  * See [LazyRowForIndexed] if you are looking for a horizontally scrolling version.
  *
- * @sample androidx.compose.foundation.samples.LazyColumnForIndexedSample
- *
  * @param items the backing list of data to display
  * @param modifier the modifier to apply to this layout
  * @param state the state object to be used to control or observe the list's state
- * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * we scrolled to the bottom.
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
  * @param horizontalAlignment the horizontal alignment applied to the items
  * @param itemContent emits the UI for an item from [items] list. It has two params: first one is
  * an index in the [items] list, and the second one is the item at this index from [items] list.
@@ -87,12 +113,24 @@
  * recompose with the real content, so even if you load the content asynchronously please reserve
  * some space for the item, for example using [Spacer].
  */
+@OptIn(InternalLayoutApi::class)
 @Composable
+@Deprecated(
+    "Use LazyColumn instead",
+    ReplaceWith(
+        "LazyColumn(modifier, state, contentPadding, horizontalAlignment = " +
+            "horizontalAlignment) { \n itemsIndexed(items, itemContent) \n }",
+        "androidx.compose.foundation.lazy.LazyColumn"
+    )
+)
 fun <T> LazyColumnForIndexed(
     items: List<T>,
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
     horizontalAlignment: Alignment.Horizontal = Alignment.Start,
     itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
 ) {
@@ -100,7 +138,9 @@
         modifier = modifier,
         state = state,
         contentPadding = contentPadding,
-        horizontalAlignment = horizontalAlignment
+        horizontalAlignment = horizontalAlignment,
+        verticalArrangement = verticalArrangement,
+        reverseLayout = reverseLayout
     ) {
         itemsIndexed(items, itemContent)
     }
@@ -112,27 +152,44 @@
  * See [LazyRowForIndexed] if you need to have both item and index params in [itemContent].
  * See [LazyColumnFor] if you are looking for a vertically scrolling version.
  *
- * @sample androidx.compose.foundation.samples.LazyRowForSample
- *
- * @param items the backing list of data to display
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
+ * @param items the backing list of data to display.
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
  * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
- * @param verticalAlignment the vertical alignment applied to the items
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param verticalAlignment the vertical alignment applied to the items.
  * @param itemContent emits the UI for an item from [items] list. May emit any number of components,
  * which will be stacked horizontally. Note that [LazyRowFor] can start scrolling incorrectly
  * if you emit nothing and then lazily recompose with the real content, so even if you load the
  * content asynchronously please reserve some space for the item, for example using [Spacer].
  * Use [LazyRowForIndexed] if you need to have both index and item params.
  */
+@OptIn(InternalLayoutApi::class)
 @Composable
+@Deprecated(
+    "Use LazyRow instead",
+    ReplaceWith(
+        "LazyRow(modifier, state, contentPadding, verticalAlignment = " +
+            "verticalAlignment) { \n items(items, itemContent) \n }",
+        "androidx.compose.foundation.lazy.LazyColumn"
+    )
+)
 fun <T> LazyRowFor(
     items: List<T>,
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal =
+        if (!reverseLayout) Arrangement.Start else Arrangement.End,
     verticalAlignment: Alignment.Vertical = Alignment.Top,
     itemContent: @Composable LazyItemScope.(T) -> Unit
 ) {
@@ -141,6 +198,8 @@
         state = state,
         contentPadding = contentPadding,
         verticalAlignment = verticalAlignment,
+        horizontalArrangement = horizontalArrangement,
+        reverseLayout = reverseLayout
     ) {
         items(items, itemContent)
     }
@@ -153,15 +212,20 @@
  *
  * See [LazyColumnForIndexed] if you are looking for a vertically scrolling version.
  *
- * @sample androidx.compose.foundation.samples.LazyRowForIndexedSample
- *
- * @param items the backing list of data to display
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
+ * @param items the backing list of data to display.
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
  * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
- * @param verticalAlignment the vertical alignment applied to the items
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param verticalAlignment the vertical alignment applied to the items.
  * @param itemContent emits the UI for an item from [items] list. It has two params: first one is
  * an index in the [items] list, and the second one is the item at this index from [items] list.
  * May emit any number of components, which will be stacked horizontally. Note that
@@ -169,12 +233,24 @@
  * recompose with the real content, so even if you load the content asynchronously please reserve
  * some space for the item, for example using [Spacer].
  */
+@OptIn(InternalLayoutApi::class)
 @Composable
+@Deprecated(
+    "Use LazyRow instead",
+    ReplaceWith(
+        "LazyRow(modifier, state, contentPadding, verticalAlignment = " +
+            "verticalAlignment) { \n itemsIndexed(items, itemContent) \n }",
+        "androidx.compose.foundation.lazy.LazyColumn"
+    )
+)
 fun <T> LazyRowForIndexed(
     items: List<T>,
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal =
+        if (!reverseLayout) Arrangement.Start else Arrangement.End,
     verticalAlignment: Alignment.Vertical = Alignment.Top,
     itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
 ) {
@@ -183,6 +259,8 @@
         state = state,
         contentPadding = contentPadding,
         verticalAlignment = verticalAlignment,
+        horizontalArrangement = horizontalArrangement,
+        reverseLayout = reverseLayout
     ) {
         itemsIndexed(items, itemContent)
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index a8f98a8..a4bc0b6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -16,52 +16,157 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.layout.WithConstraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
 
 /**
  * The DSL implementation of a lazy grid layout. It composes only visible rows of the grid.
- * This API is not stable, please consider using stable components like [LazyColumnFor] and [Row]
+ * This API is not stable, please consider using stable components like [LazyColumn] and [Row]
  * to achieve the same result.
  *
- * @param columns a fixed number of columns of the grid
+ * @param cells a class describing how cells form columns, see [GridCells] doc for more information
  * @param modifier the modifier to apply to this layout
  * @param contentPadding specify a padding around the whole content
  * @param content the [LazyListScope] which describes the content
  */
+@ExperimentalFoundationApi
 @Composable
-internal fun LazyGrid(
-    columns: Int,
+fun LazyVerticalGrid(
+    cells: GridCells,
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
-    content: LazyListScope.() -> Unit
+    content: LazyGridScope.() -> Unit
 ) {
-    val scope = LazyListScopeImpl()
+    val scope = LazyGridScopeImpl()
     scope.apply(content)
 
-    val rows = (scope.totalSize + columns - 1) / columns
+    when (cells) {
+        is GridCells.Fixed ->
+            FixedLazyGrid(
+                nColumns = cells.count,
+                modifier = modifier,
+                state = state,
+                contentPadding = contentPadding,
+                scope = scope
+            )
+        is GridCells.Adaptive ->
+            WithConstraints(
+                modifier = modifier
+            ) {
+                val nColumns = maxOf((maxWidth / cells.minSize).toInt(), 1)
+                FixedLazyGrid(
+                    nColumns = nColumns,
+                    state = state,
+                    contentPadding = contentPadding,
+                    scope = scope
+                )
+            }
+    }
+}
+
+/**
+ * This class describes how cells form columns in vertical grids or rows in horizontal grids.
+ */
+sealed class GridCells {
+    /**
+     * Combines cells with fixed number rows or columns.
+     *
+     * For example, for the vertical [LazyVerticalGrid] Fixed(3) would mean that there are 3 columns 1/3
+     * of the parent wide.
+     */
+    class Fixed(val count: Int) : GridCells()
+
+    /**
+     * Combines cells with adaptive number of rows or columns. It will try to position as many rows
+     * or columns as possible on the condition that every cell has at least [minSize] space and
+     * all extra space distributed evenly.
+     *
+     * For example, for the vertical [LazyVerticalGrid] Adaptive(20.dp) would mean that there will be as
+     * many columns as possible and every column will be at least 20.dp and all the columns will
+     * have equal width. If the screen is 88.dp wide then there will be 4 columns 22.dp each.
+     */
+    class Adaptive(val minSize: Dp) : GridCells()
+}
+
+/**
+ * Receiver scope which is used by [LazyVerticalGrid].
+ */
+interface LazyGridScope {
+    /**
+     * Adds a list of items and their content to the scope.
+     *
+     * @param items the data list
+     * @param itemContent the content displayed by a single item
+     */
+    fun <T> items(
+        items: List<T>,
+        itemContent: @Composable LazyItemScope.(item: T) -> Unit
+    )
+
+    /**
+     * Adds a single item to the scope.
+     *
+     * @param content the content of the item
+     */
+    fun item(content: @Composable LazyItemScope.() -> Unit)
+
+    /**
+     * Adds a list of items to the scope where the content of an item is aware of its index.
+     *
+     * @param items the data list
+     * @param itemContent the content displayed by a single item
+     */
+    fun <T> itemsIndexed(
+        items: List<T>,
+        itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
+    )
+}
+
+@Composable
+private fun FixedLazyGrid(
+    nColumns: Int,
+    modifier: Modifier = Modifier,
+    state: LazyListState = rememberLazyListState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    scope: LazyGridScopeImpl
+) {
+    val rows = (scope.totalSize + nColumns - 1) / nColumns
     LazyList(
         itemsCount = rows,
         modifier = modifier,
         state = state,
         contentPadding = contentPadding,
-        isVertical = true
+        isVertical = true,
+        horizontalAlignment = Alignment.Start,
+        verticalArrangement = Arrangement.Top,
+        reverseLayout = false
     ) { rowIndex ->
-        {
-            GridRow {
-                for (columnIndex in 0 until columns) {
-                    val itemIndex = rowIndex * columns + columnIndex
+        @Composable {
+            Row {
+                for (columnIndex in 0 until nColumns) {
+                    val itemIndex = rowIndex * nColumns + columnIndex
                     if (itemIndex < scope.totalSize) {
-                        scope.contentFor(itemIndex, this).invoke()
+                        GridCellBox(
+                            modifier = Modifier.weight(1f, fill = true)
+                        ) {
+                            scope.contentFor(itemIndex, this@LazyList).invoke()
+                        }
                     } else {
-                        Spacer(Modifier)
+                        Spacer(Modifier.weight(1f, fill = true))
                     }
                 }
             }
@@ -69,33 +174,57 @@
     }
 }
 
+/**
+ * TODO: Remove when the Box component supports fixed constraints.
+ */
 @Composable
-private fun GridRow(
-    content: @Composable () -> Unit
-) {
-    // TODO: Implement customisable column widths.
-    Layout(
-        content = content
-    ) { measurables, constraints ->
-
-        // TODO: Avoid int rounding to fill all the width pixels.
-        val itemConstraint = Constraints.fixedWidth(constraints.maxWidth / measurables.size)
-        var maxItemHeight = 0
-        val placeables = measurables.map { measurable ->
-            measurable.measure(itemConstraint)
-                .also {
-                    if (it.height > maxItemHeight) {
-                        maxItemHeight = it.height
-                    }
-                }
+private fun GridCellBox(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+    Layout(content, modifier) { measurables, constraints ->
+        val placeables = measurables.map { it.measure(constraints) }
+        val size = placeables.fold(IntSize.Zero) { size, item ->
+            IntSize(maxOf(size.width, item.width), maxOf(size.height, item.height))
         }
-
-        layout(constraints.maxWidth, maxItemHeight) {
-            var currentXPosition = 0
-            placeables.forEach { placeable ->
-                placeable.placeRelative(x = currentXPosition, y = 0)
-                currentXPosition += placeable.width
+        layout(constraints.constrainWidth(size.width), constraints.constrainHeight(size.height)) {
+            placeables.forEach {
+                it.place(0, 0)
             }
         }
     }
 }
+
+internal class LazyGridScopeImpl : LazyGridScope {
+    private val intervals = IntervalList<LazyItemScope.(Int) -> (@Composable () -> Unit)>()
+
+    val totalSize get() = intervals.totalSize
+
+    fun contentFor(index: Int, scope: LazyItemScope): @Composable () -> Unit {
+        val interval = intervals.intervalForIndex(index)
+        val localIntervalIndex = index - interval.startIndex
+
+        return interval.content(scope, localIntervalIndex)
+    }
+
+    override fun <T> items(
+        items: List<T>,
+        itemContent: @Composable LazyItemScope.(item: T) -> Unit
+    ) {
+        intervals.add(items.size) { index ->
+            val item = items[index]
+            @Composable { itemContent(item) }
+        }
+    }
+
+    override fun item(content: @Composable LazyItemScope.() -> Unit) {
+        intervals.add(1) { @Composable { content() } }
+    }
+
+    override fun <T> itemsIndexed(
+        items: List<T>,
+        itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
+    ) {
+        intervals.add(items.size) { index ->
+            val item = items[index]
+            @Composable { itemContent(index, item) }
+        }
+    }
+}
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 b084605..69971f10 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
@@ -16,11 +16,16 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.foundation.assertNotNestingScrollableContainers
 import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.savedinstancestate.ExperimentalRestorableStateHolder
+import androidx.compose.runtime.savedinstancestate.rememberRestorableStateHolder
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clipToBounds
@@ -29,21 +34,40 @@
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.unit.LayoutDirection
 
+@OptIn(InternalLayoutApi::class)
 @Composable
 internal fun LazyList(
+    /** The total size of the list */
     itemsCount: Int,
-    modifier: Modifier = Modifier,
+    /** Modifier to be applied for the inner layout */
+    modifier: Modifier,
+    /** State controlling the scroll position */
     state: LazyListState,
+    /** The inner padding to be added for the whole content(nor for each individual item) */
     contentPadding: PaddingValues,
-    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
-    verticalAlignment: Alignment.Vertical = Alignment.Top,
+    /** reverse the direction of scrolling and layout */
+    reverseLayout: Boolean,
+    /** The layout orientation of the list */
     isVertical: Boolean,
-    itemContentFactory: LazyItemScope.(Int) -> @Composable () -> Unit
+    /** The alignment to align items horizontally. Required when isVertical is true */
+    horizontalAlignment: Alignment.Horizontal? = null,
+    /** The vertical arrangement for items. Required when isVertical is true */
+    verticalArrangement: Arrangement.Vertical? = null,
+    /** The alignment to align items vertically. Required when isVertical is false */
+    verticalAlignment: Alignment.Vertical? = null,
+    /** The horizontal arrangement for items. Required when isVertical is false */
+    horizontalArrangement: Arrangement.Horizontal? = null,
+    /** The factory defining the content for an item on the given position in the list */
+    itemContent: LazyItemScope.(Int) -> @Composable () -> Unit
 ) {
-    val reverseDirection = AmbientLayoutDirection.current == LayoutDirection.Rtl && !isVertical
+    val isRtl = AmbientLayoutDirection.current == LayoutDirection.Rtl
+    // reverse scroll by default, to have "natural" gesture that goes reversed to layout
+    // if rtl and horizontal, do not reverse to make it right-to-left
+    val reverseScrollDirection = if (!isVertical && isRtl) reverseLayout else !reverseLayout
 
-    val cachingItemContentFactory = remember { CachingItemContentFactory(itemContentFactory) }
-    cachingItemContentFactory.itemContentFactory = itemContentFactory
+    val restorableItemContent = wrapWithStateRestoration(itemContent)
+    val cachingItemContentFactory = remember { CachingItemContentFactory(restorableItemContent) }
+    cachingItemContentFactory.itemContentFactory = restorableItemContent
 
     val startContentPadding = if (isVertical) contentPadding.top else contentPadding.start
     val endContentPadding = if (isVertical) contentPadding.bottom else contentPadding.end
@@ -51,27 +75,90 @@
         modifier
             .scrollable(
                 orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
-                // reverse scroll by default, to have "natural" gesture that goes reversed to layout
-                reverseDirection = !reverseDirection,
+                reverseDirection = reverseScrollDirection,
                 controller = state.scrollableController
             )
             .clipToBounds()
             .padding(contentPadding)
             .then(state.remeasurementModifier)
     ) { constraints ->
+        constraints.assertNotNestingScrollableContainers(isVertical)
+
         // this will update the scope object if the constrains have been changed
         cachingItemContentFactory.updateItemScope(this, constraints)
 
-        state.measure(
-            this,
+        val startContentPaddingPx = startContentPadding.toIntPx()
+        val endContentPaddingPx = endContentPadding.toIntPx()
+        val mainAxisMaxSize = (if (isVertical) constraints.maxHeight else constraints.maxWidth)
+        val spaceBetweenItemsDp = if (isVertical) {
+            requireNotNull(verticalArrangement).spacing
+        } else {
+            requireNotNull(horizontalArrangement).spacing
+        }
+        val spaceBetweenItems = spaceBetweenItemsDp.toIntPx()
+
+        val itemProvider = LazyMeasuredItemProvider(
             constraints,
             isVertical,
-            horizontalAlignment,
-            verticalAlignment,
-            startContentPadding.toIntPx(),
-            endContentPadding.toIntPx(),
-            itemsCount,
+            this,
             cachingItemContentFactory
+        ) { index, placeables ->
+            // 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.
+            val spacing = if (index.value == itemsCount - 1) 0 else spaceBetweenItems
+            LazyMeasuredItem(
+                index = index.value,
+                placeables = placeables,
+                isVertical = isVertical,
+                horizontalAlignment = horizontalAlignment,
+                verticalAlignment = verticalAlignment,
+                layoutDirection = layoutDirection,
+                startContentPadding = startContentPaddingPx,
+                endContentPadding = endContentPaddingPx,
+                spacing = spacing
+            )
+        }
+
+        val measureResult = measureLazyList(
+            itemsCount,
+            itemProvider,
+            mainAxisMaxSize,
+            startContentPaddingPx,
+            endContentPaddingPx,
+            state.firstVisibleItemIndexNonObservable,
+            state.firstVisibleItemScrollOffsetNonObservable,
+            state.scrollToBeConsumed
         )
+
+        state.applyMeasureResult(measureResult)
+
+        layoutLazyList(
+            constraints,
+            isVertical,
+            verticalArrangement,
+            horizontalArrangement,
+            measureResult,
+            reverseLayout
+        )
+    }
+}
+
+/**
+ * Converts item content factory to another one which adds auto state restoration functionality.
+ */
+@OptIn(ExperimentalRestorableStateHolder::class)
+@Composable
+internal fun wrapWithStateRestoration(
+    itemContentFactory: LazyItemScope.(Int) -> @Composable () -> Unit
+): LazyItemScope.(Int) -> @Composable () -> Unit {
+    val restorableStateHolder = rememberRestorableStateHolder<Any>()
+    return remember(itemContentFactory) {
+        { index ->
+            val content = itemContentFactory(index)
+            // we just wrap our original lambda with the one which auto restores the state
+            // currently we use index in the list as a key for the restoration, but in the future
+            // we will use the user provided key
+            (@Composable { restorableStateHolder.RestorableStateProvider(index, content) })
+        }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt
new file mode 100644
index 0000000..568f577
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+/**
+ * Contains useful information about an individual item in lazy lists like [LazyColumn] or [LazyRow].
+ *
+ * @see LazyListLayoutInfo
+ */
+interface LazyListItemInfo {
+    /**
+     * The index of the item in the list.
+     */
+    val index: Int
+
+    /**
+     * The main axis offset of the item. It is relative to the start of the lazy list container.
+     */
+    val offset: Int
+
+    /**
+     * The main axis size of the item. Note that if you emit multiple layouts in the composable
+     * slot for the item then this size will be calculated as the sum of their sizes.
+     */
+    val size: Int
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt
new file mode 100644
index 0000000..40acb47
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+/**
+ * Contains useful information about the currently displayed layout state of lazy lists like
+ * [LazyColumn] or [LazyRow]. For example you can get the list of currently displayed item.
+ *
+ * Use [LazyListState.layoutInfo] to retrieve this
+ */
+interface LazyListLayoutInfo {
+    /**
+     * The list of [LazyListItemInfo] representing all the currently visible items.
+     */
+    val visibleItemsInfo: List<LazyListItemInfo>
+
+    /**
+     * The start offset of the layout's viewport. You can think of it as a minimum offset which
+     * would be visible. Usually it is 0, but it can be negative if a content padding was applied
+     * as the content displayed in the content padding area is still visible.
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportStartOffset: Int
+
+    /**
+     * The end offset of the layout's viewport. You can think of it as a maximum offset which
+     * would be visible. Usually it is a size of the lazy list container plus a content padding.
+     *
+     * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+     */
+    val viewportEndOffset: Int
+
+    /**
+     * The total count of items passed to [LazyColumn] or [LazyRow].
+     */
+    val totalItemsCount: Int
+}
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
new file mode 100644
index 0000000..3e4670e
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Measures and calculates the positions for the currently visible items. The result is produced
+ * as a [LazyListMeasureResult] which contains all the calculations.
+ */
+internal fun measureLazyList(
+    itemsCount: Int,
+    itemProvider: LazyMeasuredItemProvider,
+    mainAxisMaxSize: Int,
+    startContentPadding: Int,
+    endContentPadding: Int,
+    firstVisibleItemIndex: DataIndex,
+    firstVisibleItemScrollOffset: Int,
+    scrollToBeConsumed: Float
+): LazyListMeasureResult {
+    require(startContentPadding >= 0)
+    require(endContentPadding >= 0)
+    if (itemsCount <= 0) {
+        // empty data set. reset the current scroll and report zero size
+        return LazyListMeasureResult(
+            mainAxisSize = 0,
+            crossAxisSize = 0,
+            items = emptyList(),
+            itemsScrollOffset = 0,
+            firstVisibleItemIndex = DataIndex(0),
+            firstVisibleItemScrollOffset = 0,
+            canScrollForward = false,
+            consumedScroll = 0f,
+            viewportStartOffset = -startContentPadding,
+            viewportEndOffset = endContentPadding,
+            totalItemsCount = 0
+        )
+    } else {
+        var currentFirstItemIndex = firstVisibleItemIndex
+        var currentFirstItemScrollOffset = firstVisibleItemScrollOffset
+        if (currentFirstItemIndex.value >= itemsCount) {
+            // the data set has been updated and now we have less items that we were
+            // scrolled to before
+            currentFirstItemIndex = DataIndex(itemsCount - 1)
+            currentFirstItemScrollOffset = 0
+        }
+
+        // represents the real amount of scroll we applied as a result of this measure pass.
+        var scrollDelta = scrollToBeConsumed.roundToInt()
+
+        // applying the whole requested scroll offset. we will figure out if we can't consume
+        // all of it later
+        currentFirstItemScrollOffset -= scrollDelta
+
+        // if the current scroll offset is less than minimally possible
+        if (currentFirstItemIndex == DataIndex(0) && currentFirstItemScrollOffset < 0) {
+            scrollDelta += currentFirstItemScrollOffset
+            currentFirstItemScrollOffset = 0
+        }
+
+        // saving it into the field as we first go backward and after that want to go forward
+        // again from the initial position
+        val goingForwardInitialIndex = currentFirstItemIndex
+        var goingForwardInitialScrollOffset = currentFirstItemScrollOffset
+
+        // this will contain all the MeasuredItems representing the visible items
+        val visibleItems = mutableListOf<LazyMeasuredItem>()
+
+        // include the start padding so we compose items in the padding area. in the end we
+        // will remove it back from the currentFirstItemScrollOffset calculation
+        currentFirstItemScrollOffset -= startContentPadding
+
+        // define min and max offsets (min offset currently includes startPadding)
+        val minOffset = -startContentPadding
+        val maxOffset = mainAxisMaxSize
+
+        // max of cross axis sizes of all visible items
+        var maxCrossAxis = 0
+
+        // we had scrolled backward or we compose items in the start padding area, which means
+        // items before current firstItemScrollOffset should be visible. compose them and update
+        // firstItemScrollOffset
+        while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
+            val previous = DataIndex(currentFirstItemIndex.value - 1)
+            val measuredItem = itemProvider.getAndMeasure(previous)
+            visibleItems.add(0, measuredItem)
+            maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+            currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+            currentFirstItemIndex = previous
+        }
+        // if we were scrolled backward, but there were not enough items before. this means
+        // not the whole scroll was consumed
+        if (currentFirstItemScrollOffset < minOffset) {
+            scrollDelta += currentFirstItemScrollOffset
+            goingForwardInitialScrollOffset += currentFirstItemScrollOffset
+            currentFirstItemScrollOffset = minOffset
+        }
+
+        // remembers the composed MeasuredItem which we are not currently placing as they are out
+        // of screen. it is possible we will need to place them if the remaining items will
+        // not fill the whole viewport and we will need to scroll back
+        var notUsedButComposedItems: MutableList<LazyMeasuredItem>? = null
+
+        // composing visible items starting from goingForwardInitialIndex until we fill the
+        // whole viewport
+        var index = goingForwardInitialIndex
+        val maxMainAxis = maxOffset + endContentPadding
+        var mainAxisUsed = -goingForwardInitialScrollOffset
+        while (mainAxisUsed <= maxMainAxis && index.value < itemsCount) {
+            val measuredItem = itemProvider.getAndMeasure(index)
+            mainAxisUsed += measuredItem.sizeWithSpacings
+
+            if (mainAxisUsed < minOffset) {
+                // this item is offscreen and will not be placed. advance firstVisibleItemIndex
+                currentFirstItemIndex = index + 1
+                currentFirstItemScrollOffset -= measuredItem.sizeWithSpacings
+                // but remember the corresponding placeables in case we will be forced to
+                // scroll back as there were not enough items to fill the viewport
+                if (notUsedButComposedItems == null) {
+                    notUsedButComposedItems = mutableListOf()
+                }
+                notUsedButComposedItems.add(measuredItem)
+            } else {
+                maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+                visibleItems.add(measuredItem)
+            }
+
+            index++
+        }
+
+        // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
+        // lets try to scroll back if we have enough items before firstVisibleItemIndex.
+        if (mainAxisUsed < maxOffset) {
+            val toScrollBack = maxOffset - mainAxisUsed
+            currentFirstItemScrollOffset -= toScrollBack
+            mainAxisUsed += toScrollBack
+            while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
+                val previous = DataIndex(currentFirstItemIndex.value - 1)
+                val alreadyComposedIndex = notUsedButComposedItems?.lastIndex ?: -1
+                val measuredItem = if (alreadyComposedIndex >= 0) {
+                    notUsedButComposedItems!!.removeAt(alreadyComposedIndex)
+                } else {
+                    itemProvider.getAndMeasure(previous)
+                }
+                visibleItems.add(0, measuredItem)
+                maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+                currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+                currentFirstItemIndex = previous
+            }
+            scrollDelta += toScrollBack
+            if (currentFirstItemScrollOffset < minOffset) {
+                scrollDelta += currentFirstItemScrollOffset
+                mainAxisUsed += currentFirstItemScrollOffset
+                currentFirstItemScrollOffset = minOffset
+            }
+        }
+
+        // report the amount of pixels we consumed. scrollDelta can be smaller than
+        // scrollToBeConsumed if there were not enough items to fill the offered space or it
+        // can be larger if items were resized, or if, for example, we were previously
+        // displaying the item 15, but now we have only 10 items in total in the data set.
+        val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+            abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+        ) {
+            scrollDelta.toFloat()
+        } else {
+            scrollToBeConsumed
+        }
+
+        // the initial offset for items from visibleItems list
+        val firstItemOffset = -(currentFirstItemScrollOffset + startContentPadding)
+
+        // compensate the content padding we initially added in currentFirstItemScrollOffset.
+        // if the item is fully located in the start padding area we  need to use the next
+        // item as a value for currentFirstItemIndex
+        if (startContentPadding > 0) {
+            currentFirstItemScrollOffset += startContentPadding
+            var startPaddingItems = 0
+            while (startPaddingItems < visibleItems.lastIndex) {
+                val size = visibleItems[startPaddingItems].sizeWithSpacings
+                if (size <= currentFirstItemScrollOffset) {
+                    startPaddingItems++
+                    currentFirstItemScrollOffset -= size
+                    currentFirstItemIndex++
+                } else {
+                    break
+                }
+            }
+        }
+
+        val mainAxisSize = mainAxisUsed + startContentPadding
+        val maximumVisibleOffset = minOf(mainAxisSize, mainAxisMaxSize) + endContentPadding
+        return LazyListMeasureResult(
+            mainAxisSize = mainAxisSize,
+            crossAxisSize = maxCrossAxis,
+            items = visibleItems,
+            itemsScrollOffset = firstItemOffset,
+            firstVisibleItemIndex = currentFirstItemIndex,
+            firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
+            canScrollForward = mainAxisUsed > maxOffset,
+            consumedScroll = consumedScroll,
+            viewportStartOffset = -startContentPadding,
+            viewportEndOffset = maximumVisibleOffset,
+            totalItemsCount = itemsCount
+        )
+    }
+}
+
+/**
+ * Lays out [LazyMeasuredItem]s based on the [LazyListMeasureResult] and the passed arrangement.
+ */
+@OptIn(InternalLayoutApi::class)
+internal fun MeasureScope.layoutLazyList(
+    constraints: Constraints,
+    isVertical: Boolean,
+    verticalArrangement: Arrangement.Vertical?,
+    horizontalArrangement: Arrangement.Horizontal?,
+    measureResult: LazyListMeasureResult,
+    reverseLayout: Boolean
+): MeasureResult {
+    val layoutWidth = constraints.constrainWidth(
+        if (isVertical) measureResult.crossAxisSize else measureResult.mainAxisSize
+    )
+    val layoutHeight = constraints.constrainHeight(
+        if (isVertical) measureResult.mainAxisSize else measureResult.crossAxisSize
+    )
+    val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+    val hasSpareSpace = measureResult.mainAxisSize < mainAxisLayoutSize
+    if (hasSpareSpace) {
+        check(measureResult.itemsScrollOffset == 0)
+    }
+    val density = this
+
+    return layout(layoutWidth, layoutHeight) {
+        var currentMainAxis = measureResult.itemsScrollOffset
+        if (hasSpareSpace) {
+            val items = if (reverseLayout) measureResult.items.reversed() else measureResult.items
+            val sizes = IntArray(items.size) { index ->
+                items[index].size
+            }
+            val positions = IntArray(items.size) { 0 }
+            if (isVertical) {
+                requireNotNull(verticalArrangement)
+                    .arrange(mainAxisLayoutSize, sizes, density, positions)
+            } else {
+                requireNotNull(horizontalArrangement)
+                    .arrange(mainAxisLayoutSize, sizes, layoutDirection, density, positions)
+            }
+            positions.forEachIndexed { index, position ->
+                items[index].place(this, layoutWidth, layoutHeight, position, reverseLayout)
+            }
+        } else {
+            measureResult.items.fastForEach {
+                val offset = if (reverseLayout) {
+                    mainAxisLayoutSize - currentMainAxis - (it.size)
+                } else {
+                    currentMainAxis
+                }
+                it.place(this, layoutWidth, layoutHeight, offset, reverseLayout)
+                currentMainAxis += it.sizeWithSpacings
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..02b13b3
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+/**
+ * The result of the measure pass for lazy list layout.
+ */
+internal class LazyListMeasureResult(
+    /** Calculated size for the main axis.*/
+    val mainAxisSize: Int,
+    /** Calculated size for the cross axis.*/
+    val crossAxisSize: Int,
+    /** The list of items to be placed during the layout pass.*/
+    val items: List<LazyMeasuredItem>,
+    /** The main axis offset to be used for the first item in the [items] list.*/
+    val itemsScrollOffset: Int,
+    /** The new value for [LazyListState.firstVisibleItemIndex].*/
+    val firstVisibleItemIndex: DataIndex,
+    /** The new value for [LazyListState.firstVisibleItemScrollOffset].*/
+    val firstVisibleItemScrollOffset: Int,
+    /** True if there is some space available to continue scrolling in the forward direction.*/
+    val canScrollForward: Boolean,
+    /** The amount of scroll consumed during the measure pass.*/
+    val consumedScroll: Float,
+    override val viewportStartOffset: Int,
+    override val viewportEndOffset: Int,
+    override val totalItemsCount: Int
+) : LazyListLayoutInfo {
+    override val visibleItemsInfo: List<LazyListItemInfo> get() = items
+}
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 456a124..a6d983b 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
@@ -18,15 +18,13 @@
 
 import androidx.compose.animation.asDisposableClock
 import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.spring
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.animation.FlingConfig
 import androidx.compose.foundation.animation.defaultFlingConfig
-import androidx.compose.foundation.assertNotNestingScrollableContainers
 import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.Scrollable
 import androidx.compose.foundation.gestures.ScrollableController
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
@@ -35,34 +33,12 @@
 import androidx.compose.runtime.savedinstancestate.Saver
 import androidx.compose.runtime.savedinstancestate.listSaver
 import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.Remeasurement
 import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.layout.SubcomposeMeasureScope
 import androidx.compose.ui.platform.AmbientAnimationClock
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.util.annotation.IntRange
 import androidx.compose.ui.util.annotation.VisibleForTesting
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastSumBy
 import kotlin.math.abs
-import kotlin.math.roundToInt
-import kotlin.math.sign
-
-@Suppress("NOTHING_TO_INLINE", "EXPERIMENTAL_FEATURE_WARNING")
-internal inline class DataIndex(val value: Int) {
-    inline operator fun inc(): DataIndex = DataIndex(value + 1)
-    inline operator fun dec(): DataIndex = DataIndex(value - 1)
-    inline operator fun plus(i: Int): DataIndex = DataIndex(value + i)
-    inline operator fun minus(i: Int): DataIndex = DataIndex(value - i)
-    inline operator fun minus(i: DataIndex): DataIndex = DataIndex(value - i.value)
-    inline operator fun compareTo(other: DataIndex): Int = value - other.value
-}
 
 /**
  * Creates a [LazyListState] that is remembered across compositions.
@@ -122,7 +98,7 @@
     interactionState: InteractionState? = null,
     flingConfig: FlingConfig,
     animationClock: AnimationClockObservable
-) {
+) : Scrollable {
     /**
      * The holder class for the current scroll position.
      */
@@ -141,7 +117,7 @@
     val firstVisibleItemScrollOffset: Int get() = scrollPosition.observableScrollOffset
 
     /**
-     * whether this [LazyListState] is currently scrolling via [scroll] or via an
+     * Whether this [LazyListState] is currently scrolling via [scroll] or via an
      * animation/fling.
      *
      * Note: **all** scrolls initiated via [scroll] are considered to be animations, regardless of
@@ -150,11 +126,31 @@
     val isAnimationRunning
         get() = scrollableController.isAnimationRunning
 
+    /** Backing state for [layoutInfo] */
+    private val layoutInfoState = mutableStateOf<LazyListLayoutInfo>(EmptyLazyListLayoutInfo)
+
+    /**
+     * The object of [LazyListLayoutInfo] calculated during the last layout pass. For example,
+     * you can use it to calculate what items are currently visible.
+     */
+    val layoutInfo: LazyListLayoutInfo get() = layoutInfoState.value
+
     /**
      * The amount of scroll to be consumed in the next layout pass.  Scrolling forward is negative
      * - that is, it is the amount that the items are offset in y
      */
-    private var scrollToBeConsumed = 0f
+    internal var scrollToBeConsumed = 0f
+        private set
+
+    /**
+     * The same as [firstVisibleItemIndex] but the read will not trigger remeasure.
+     */
+    internal val firstVisibleItemIndexNonObservable: DataIndex get() = scrollPosition.index
+
+    /**
+     * The same as [firstVisibleItemScrollOffset] but the read will not trigger remeasure.
+     */
+    internal val firstVisibleItemScrollOffsetNonObservable: Int get() = scrollPosition.scrollOffset
 
     /**
      * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
@@ -179,6 +175,7 @@
      */
     @VisibleForTesting
     internal var numMeasurePasses: Int = 0
+        private set
 
     /**
      * The modifier which provides [remeasurement].
@@ -227,26 +224,10 @@
      * If [scroll] is called from elsewhere, this will be canceled.
      */
     @OptIn(ExperimentalFoundationApi::class)
-    suspend fun scroll(
+    override suspend fun scroll(
         block: suspend ScrollScope.() -> Unit
     ): Unit = scrollableController.scroll(block)
 
-    /**
-     * Smooth scroll by [value] pixels.
-     *
-     * Cancels the currently running scroll, if any, and suspends until the cancellation is
-     * complete.
-     *
-     * @param value delta to scroll by
-     * @param spec [AnimationSpec] to be used for this smooth scrolling
-     *
-     * @return the amount of scroll consumed
-     */
-    suspend fun smoothScrollBy(
-        value: Float,
-        spec: AnimationSpec<Float> = spring()
-    ): Float = scrollableController.smoothScrollBy(value, spec)
-
     // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
     //  fine-grained control over scrolling
     @VisibleForTesting
@@ -284,245 +265,19 @@
     }
 
     /**
-     * Measures and positions currently visible items using [itemContentFactory] for subcomposing.
+     *  Updates the state with the new calculated scroll position and consumed scroll.
      */
-    internal fun measure(
-        scope: SubcomposeMeasureScope,
-        constraints: Constraints,
-        isVertical: Boolean,
-        horizontalAlignment: Alignment.Horizontal,
-        verticalAlignment: Alignment.Vertical,
-        startContentPadding: Int,
-        endContentPadding: Int,
-        itemsCount: Int,
-        itemContentFactory: (Int) -> @Composable () -> Unit
-    ): MeasureResult = with(scope) {
+    internal fun applyMeasureResult(measureResult: LazyListMeasureResult) {
+        scrollPosition.update(
+            index = measureResult.firstVisibleItemIndex,
+            scrollOffset = measureResult.firstVisibleItemScrollOffset,
+            canScrollForward = measureResult.canScrollForward
+        )
+        scrollToBeConsumed -= measureResult.consumedScroll
+        layoutInfoState.value = measureResult
         numMeasurePasses++
-        constraints.assertNotNestingScrollableContainers(isVertical)
-        require(startContentPadding >= 0)
-        require(endContentPadding >= 0)
-        if (itemsCount <= 0) {
-            // empty data set. reset the current scroll and report zero size
-            scrollPosition.update(
-                index = DataIndex(0),
-                scrollOffset = 0,
-                canScrollForward = false
-            )
-            layout(constraints.constrainWidth(0), constraints.constrainHeight(0)) {}
-        } else {
-            var currentFirstItemIndex = scrollPosition.index
-            var currentFirstItemScrollOffset = scrollPosition.scrollOffset
-
-            if (currentFirstItemIndex.value >= itemsCount) {
-                // the data set has been updated and now we have less items that we were
-                // scrolled to before
-                currentFirstItemIndex = DataIndex(itemsCount - 1)
-                currentFirstItemScrollOffset = 0
-            }
-
-            // represents the real amount of scroll we applied as a result of this measure pass.
-            var scrollDelta = scrollToBeConsumed.roundToInt()
-
-            // applying the whole requested scroll offset. we will figure out if we can't consume
-            // all of it later
-            currentFirstItemScrollOffset -= scrollDelta
-
-            // if the current scroll offset is less than minimally possible
-            if (currentFirstItemIndex == DataIndex(0) && currentFirstItemScrollOffset < 0) {
-                scrollDelta += currentFirstItemScrollOffset
-                currentFirstItemScrollOffset = 0
-            }
-
-            // the constraints we will measure child with. the cross axis are not restricted
-            val childConstraints = Constraints(
-                maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
-                maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
-            )
-            // saving it into the field as we first go backward and after that want to go forward
-            // again from the initial position
-            val goingForwardInitialIndex = currentFirstItemIndex
-            var goingForwardInitialScrollOffset = currentFirstItemScrollOffset
-
-            // this will contain all the placeables representing the visible items
-            val visibleItemsPlaceables = mutableListOf<List<Placeable>>()
-
-            // include the start padding so we compose items in the padding area. in the end we
-            // will remove it back from the currentFirstItemScrollOffset calculation
-            currentFirstItemScrollOffset -= startContentPadding
-
-            // define min and max offsets (min offset currently includes startPadding)
-            val minOffset = -startContentPadding
-            val maxOffset = (if (isVertical) constraints.maxHeight else constraints.maxWidth)
-
-            // we had scrolled backward or we compose items in the start padding area, which means
-            // items before current firstItemScrollOffset should be visible. compose them and update
-            // firstItemScrollOffset
-            while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
-                val previous = DataIndex(currentFirstItemIndex.value - 1)
-                val placeables =
-                    subcompose(previous, itemContentFactory(previous.value)).fastMap {
-                        it.measure(childConstraints)
-                    }
-                visibleItemsPlaceables.add(0, placeables)
-                currentFirstItemScrollOffset += placeables.mainAxisSize(isVertical)
-                currentFirstItemIndex = previous
-            }
-            // if we were scrolled backward, but there were not enough items before. this means
-            // not the whole scroll was consumed
-            if (currentFirstItemScrollOffset < minOffset) {
-                scrollDelta += currentFirstItemScrollOffset
-                goingForwardInitialScrollOffset += currentFirstItemScrollOffset
-                currentFirstItemScrollOffset = minOffset
-            }
-
-            // remembers the composed placeables which we are not currently placing as they are out
-            // of screen. it is possible we will need to place them if the remaining items will
-            // not fill the whole viewport and we will need to scroll back
-            var notUsedButComposedItems: MutableList<List<Placeable>>? = null
-
-            // composing visible items starting from goingForwardInitialIndex until we fill the
-            // whole viewport
-            var index = goingForwardInitialIndex
-            val maxMainAxis = maxOffset + endContentPadding
-            var mainAxisUsed = -goingForwardInitialScrollOffset
-            var maxCrossAxis = 0
-            while (mainAxisUsed <= maxMainAxis && index.value < itemsCount) {
-                val placeables =
-                    subcompose(index, itemContentFactory(index.value)).fastMap {
-                        it.measure(childConstraints)
-                    }
-                var size = 0
-                placeables.fastForEach {
-                    size += if (isVertical) it.height else it.width
-                    maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
-                }
-                mainAxisUsed += size
-
-                if (mainAxisUsed < minOffset) {
-                    // this item is offscreen and will not be placed. advance firstVisibleItemIndex
-                    currentFirstItemIndex = index + 1
-                    currentFirstItemScrollOffset -= size
-                    // but remember the corresponding placeables in case we will be forced to
-                    // scroll back as there were not enough items to fill the viewport
-                    if (notUsedButComposedItems == null) {
-                        notUsedButComposedItems = mutableListOf()
-                    }
-                    notUsedButComposedItems.add(placeables)
-                } else {
-                    visibleItemsPlaceables.add(placeables)
-                }
-
-                index++
-            }
-
-            // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
-            // lets try to scroll back if we have enough items before firstVisibleItemIndex.
-            if (mainAxisUsed < maxOffset) {
-                val toScrollBack = maxOffset - mainAxisUsed
-                currentFirstItemScrollOffset -= toScrollBack
-                mainAxisUsed += toScrollBack
-                while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
-                    val previous = DataIndex(currentFirstItemIndex.value - 1)
-                    val alreadyComposedIndex = notUsedButComposedItems?.lastIndex ?: -1
-                    val placeables = if (alreadyComposedIndex >= 0) {
-                        notUsedButComposedItems!!.removeAt(alreadyComposedIndex)
-                    } else {
-                        subcompose(previous, itemContentFactory(previous.value)).fastMap {
-                            it.measure(childConstraints)
-                        }
-                    }
-                    visibleItemsPlaceables.add(0, placeables)
-                    val size = placeables.mainAxisSize(isVertical)
-                    currentFirstItemScrollOffset += size
-                    currentFirstItemIndex = previous
-                }
-                scrollDelta += toScrollBack
-                if (currentFirstItemScrollOffset < minOffset) {
-                    scrollDelta += currentFirstItemScrollOffset
-                    mainAxisUsed += currentFirstItemScrollOffset
-                    currentFirstItemScrollOffset = minOffset
-                }
-            }
-
-            // report the amount of pixels we consumed. scrollDelta can be smaller than
-            // scrollToBeConsumed if there were not enough items to fill the offered space or it
-            // can be larger if items were resized, or if, for example, we were previously
-            // displaying the item 15, but now we have only 10 items in total in the data set.
-            if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
-                abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
-            ) {
-                scrollToBeConsumed -= scrollDelta
-            } else {
-                scrollToBeConsumed = 0f
-            }
-
-            // Wrap the content of the children
-            val layoutWidth = constraints.constrainWidth(
-                if (isVertical) maxCrossAxis else mainAxisUsed + startContentPadding
-            )
-            val layoutHeight = constraints.constrainHeight(
-                if (!isVertical) maxCrossAxis else mainAxisUsed + startContentPadding
-            )
-
-            // the initial offset for placeables in visibleItemsPlaceables
-            val firstPlaceableOffset = -(currentFirstItemScrollOffset + startContentPadding)
-
-            // compensate the content padding we initially added in currentFirstItemScrollOffset.
-            // if the item is fully located in the start padding area we  need to use the next
-            // item as a value for currentFirstItemIndex
-            if (startContentPadding > 0) {
-                currentFirstItemScrollOffset += startContentPadding
-                var startPaddingItems = 0
-                while (startPaddingItems < visibleItemsPlaceables.lastIndex) {
-                    val size = visibleItemsPlaceables[startPaddingItems].mainAxisSize(isVertical)
-                    if (size <= currentFirstItemScrollOffset) {
-                        startPaddingItems++
-                        currentFirstItemScrollOffset -= size
-                        currentFirstItemIndex++
-                    } else {
-                        break
-                    }
-                }
-            }
-
-            // update state with the new calculated scroll position
-            scrollPosition.update(
-                index = currentFirstItemIndex,
-                scrollOffset = currentFirstItemScrollOffset,
-                canScrollForward = mainAxisUsed > maxOffset
-            )
-
-            return layout(layoutWidth, layoutHeight) {
-                var currentMainAxis = firstPlaceableOffset
-                visibleItemsPlaceables.fastForEach { placeables ->
-                    placeables.fastForEach {
-                        if (isVertical) {
-                            val x =
-                                horizontalAlignment.align(it.width, layoutWidth, layoutDirection)
-                            if (currentMainAxis + it.height > minOffset &&
-                                currentMainAxis < layoutHeight + endContentPadding
-                            ) {
-                                it.placeWithLayer(x, currentMainAxis)
-                            }
-                            currentMainAxis += it.height
-                        } else {
-                            val y = verticalAlignment.align(it.height, layoutHeight)
-                            if (currentMainAxis + it.width > minOffset &&
-                                currentMainAxis < layoutWidth + endContentPadding
-                            ) {
-                                it.placeRelativeWithLayer(currentMainAxis, y)
-                            }
-                            currentMainAxis += it.width
-                        }
-                    }
-                }
-            }
-        }
     }
 
-    private fun List<Placeable>.mainAxisSize(isVertical: Boolean) =
-        fastSumBy { if (isVertical) it.height else it.width }
-
     companion object {
         /**
          * The default [Saver] implementation for [LazyListState].
@@ -591,3 +346,10 @@
         this.canScrollForward = canScrollForward
     }
 }
+
+private object EmptyLazyListLayoutInfo : LazyListLayoutInfo {
+    override val visibleItemsInfo = emptyList<LazyListItemInfo>()
+    override val viewportStartOffset = 0
+    override val viewportEndOffset = 0
+    override val totalItemsCount = 0
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
new file mode 100644
index 0000000..16a5ac9
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * Represents one measured item of the lazy list. It can in fact consist of multiple placeables
+ * if the user emit multiple layout nodes in the item callback.
+ */
+internal class LazyMeasuredItem(
+    override val index: Int,
+    private val placeables: List<Placeable>,
+    private val isVertical: Boolean,
+    private val horizontalAlignment: Alignment.Horizontal?,
+    private val verticalAlignment: Alignment.Vertical?,
+    private val layoutDirection: LayoutDirection,
+    private val startContentPadding: Int,
+    private val endContentPadding: Int,
+    /**
+     * Extra spacing to be added to [size] aside from the sum of the [placeables] size. It
+     * is usually representing the spacing after the item.
+     */
+    private val spacing: Int
+) : LazyListItemInfo {
+    /**
+     * Sum of the main axis sizes of all the inner placeables.
+     */
+    override val size: Int
+
+    /**
+     * Sum of the main axis sizes of all the inner placeables and [spacing].
+     */
+    val sizeWithSpacings: Int
+
+    /**
+     * Max of the cross axis sizes of all the inner placeables.
+     */
+    val crossAxisSize: Int
+
+    override var offset: Int = 0
+        private set
+
+    init {
+        var mainAxisSize = 0
+        var maxCrossAxis = 0
+        placeables.fastForEach {
+            mainAxisSize += if (isVertical) it.height else it.width
+            maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
+        }
+        size = mainAxisSize
+        sizeWithSpacings = size + spacing
+        crossAxisSize = maxCrossAxis
+    }
+
+    /**
+     * Perform placing for all the inner placeables at [offset] main axis position. [layoutWidth]
+     * and [layoutHeight] should be provided to not place placeables which are ended up outside of
+     * the viewport (for example one item consist of 2 placeables, and the first one is not going
+     * to be visible, so we don't place it as an optimization, but place the second one).
+     * If [reverseOrder] is true the inner placeables would be placed in the inverted order.
+     */
+    fun place(
+        scope: Placeable.PlacementScope,
+        layoutWidth: Int,
+        layoutHeight: Int,
+        offset: Int,
+        reverseOrder: Boolean
+    ) = with(scope) {
+        this@LazyMeasuredItem.offset = offset
+        var mainAxisOffset = offset
+        val indices = if (reverseOrder) placeables.lastIndex downTo 0 else placeables.indices
+        for (index in indices) {
+            val it = placeables[index]
+            if (isVertical) {
+                val x = requireNotNull(horizontalAlignment)
+                    .align(it.width, layoutWidth, layoutDirection)
+                if (mainAxisOffset + it.height > -startContentPadding &&
+                    mainAxisOffset < layoutHeight + endContentPadding
+                ) {
+                    it.placeWithLayer(x, mainAxisOffset)
+                }
+                mainAxisOffset += it.height
+            } else {
+                val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
+                if (mainAxisOffset + it.width > -startContentPadding &&
+                    mainAxisOffset < layoutWidth + endContentPadding
+                ) {
+                    it.placeRelativeWithLayer(mainAxisOffset, y)
+                }
+                mainAxisOffset += it.width
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
new file mode 100644
index 0000000..89ae7d7
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeMeasureScope
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.util.fastMap
+
+/**
+ * Abstracts away the subcomposition from the measuring logic.
+ */
+internal class LazyMeasuredItemProvider(
+    constraints: Constraints,
+    isVertical: Boolean,
+    private val scope: SubcomposeMeasureScope,
+    private val itemContentFactory: (Int) -> @Composable () -> Unit,
+    private val measuredItemFactory: (DataIndex, List<Placeable>) -> LazyMeasuredItem
+) {
+    // the constraints we will measure child with. the main axis is not restricted
+    private val childConstraints = Constraints(
+        maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
+        maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
+    )
+
+    /**
+     * Used to subcompose items of lazy lists. Composed placeables will be measured with the
+     * correct constraints and wrapped into [LazyMeasuredItem].
+     * This method can be called only once with each [index] per the measure pass.
+     */
+    fun getAndMeasure(index: DataIndex): LazyMeasuredItem {
+        val placeables = scope.subcompose(index, itemContentFactory(index.value)).fastMap {
+            it.measure(childConstraints)
+        }
+        return measuredItemFactory(index, placeables)
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
index 22fd657d..a6df531 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
@@ -27,7 +27,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.semantics.accessibilityValue
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.selected
 import androidx.compose.ui.semantics.semantics
 
@@ -71,7 +71,7 @@
             onClick = onClick
         ).semantics {
             this.selected = selected
-            this.accessibilityValue = if (selected) Strings.Selected else Strings.NotSelected
+            this.stateDescription = if (selected) Strings.Selected else Strings.NotSelected
         }
     },
     inspectorInfo = debugInspectorInfo {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
index acde3bd..356f5e1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
@@ -30,7 +30,7 @@
 import androidx.compose.ui.gesture.pressIndicatorGestureFilter
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.semantics.accessibilityValue
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.semantics
@@ -137,7 +137,7 @@
 ): Modifier = composed {
     // TODO(pavlis): Handle multiple states for Semantics
     val semantics = Modifier.semantics(mergeDescendants = true) {
-        this.accessibilityValue = when (state) {
+        this.stateDescription = when (state) {
             // TODO(ryanmentley): These should be set by Checkbox, Switch, etc.
             On -> Strings.Checked
             Off -> Strings.Unchecked
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 57b9d1b..cec507f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.Interaction
+import androidx.compose.foundation.InteractionState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -92,6 +94,10 @@
  * communicating with platform text input service, e.g. software keyboard on Android. Called with
  * [SoftwareKeyboardController] instance which can be used for requesting input show/hide software
  * keyboard.
+ * @param interactionState the [InteractionState] representing the different [Interaction]s
+ * present on this TextField. You can create and pass in your own remembered [InteractionState]
+ * if you want to read the [InteractionState] and customize the appearance / behavior of this
+ * TextField in different [Interaction]s.
  * @param cursorColor Color of the cursor. If [Color.Unspecified], there will be no cursor drawn
  */
 @OptIn(ExperimentalTextApi::class)
@@ -108,6 +114,7 @@
     visualTransformation: VisualTransformation = VisualTransformation.None,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     onTextInputStarted: (SoftwareKeyboardController) -> Unit = {},
+    interactionState: InteractionState = remember { InteractionState() },
     cursorColor: Color = Color.Black
 ) {
     var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
@@ -130,6 +137,7 @@
         onTextLayout = onTextLayout,
         onTextInputStarted = onTextInputStarted,
         cursorColor = cursorColor,
+        interactionState = interactionState,
         singleLine = singleLine
     )
 }
@@ -188,6 +196,10 @@
  * communicating with platform text input service, e.g. software keyboard on Android. Called with
  * [SoftwareKeyboardController] instance which can be used for requesting input show/hide software
  * keyboard.
+ * @param interactionState The [InteractionState] representing the different [Interaction]s
+ * present on this TextField. You can create and pass in your own remembered [InteractionState]
+ * if you want to read the [InteractionState] and customize the appearance / behavior of this
+ * TextField in different [Interaction]s.
  * @param cursorColor Color of the cursor. If [Color.Unspecified], there will be no cursor drawn
  */
 @Composable
@@ -204,6 +216,7 @@
     visualTransformation: VisualTransformation = VisualTransformation.None,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     onTextInputStarted: (SoftwareKeyboardController) -> Unit = {},
+    interactionState: InteractionState = remember { InteractionState() },
     cursorColor: Color = Color.Black
 ) {
     // We use it to get the cursor position
@@ -224,6 +237,7 @@
             textLayoutResult.value = it
             onTextLayout(it)
         },
+        interactionState = interactionState,
         onTextInputStarted = onTextInputStarted,
         cursorColor = cursorColor,
         imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
@@ -235,6 +249,7 @@
                 scrollerPosition,
                 value,
                 visualTransformation,
+                interactionState,
                 textLayoutResult
             )
     )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index a39fc33..8638678 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -52,6 +52,7 @@
 import androidx.compose.ui.semantics.getTextLayoutResult
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.Placeholder
 import androidx.compose.ui.text.TextDelegate
@@ -96,6 +97,7 @@
  */
 @Composable
 @InternalTextApi
+@OptIn(ExperimentalTextApi::class)
 fun CoreText(
     text: AnnotatedString,
     modifier: Modifier = Modifier,
@@ -194,7 +196,10 @@
     }
 }
 
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 private class TextController(val state: TextState) {
     var selectionRegistrar: SelectionRegistrar? = null
 
@@ -225,7 +230,7 @@
             if (state.selectionRange != null) {
                 val newGlobalPosition = it.globalPosition
                 if (newGlobalPosition != state.previousGlobalPosition) {
-                    selectionRegistrar.onPositionChange()
+                    selectionRegistrar.notifyPositionChange()
                 }
                 state.previousGlobalPosition = newGlobalPosition
             }
@@ -353,7 +358,7 @@
     var layoutCoordinates: LayoutCoordinates? = null
     /** The latest TextLayoutResult calculated in the measure block */
     var layoutResult: TextLayoutResult? = null
-    /** The global position calculated during the last onPositioned callback */
+    /** The global position calculated during the last notifyPosition callback */
     var previousGlobalPosition: Offset = Offset.Zero
     /** The paint used to draw highlight background for selected text. */
     val selectionPaint: Paint = Paint()
@@ -433,7 +438,10 @@
     return Pair(placeholders, inlineComposables)
 }
 
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 @VisibleForTesting
 internal fun longPressDragObserver(
     state: TextState,
@@ -455,10 +463,9 @@
             state.layoutCoordinates?.let {
                 if (!it.isAttached) return
 
-                selectionRegistrar?.onUpdateSelection(
+                selectionRegistrar?.notifySelectionUpdateStart(
                     layoutCoordinates = it,
-                    startPosition = pxPosition,
-                    endPosition = pxPosition
+                    startPosition = pxPosition
                 )
 
                 dragBeginPosition = pxPosition
@@ -466,7 +473,6 @@
         }
 
         override fun onDragStart() {
-            super.onDragStart()
             // selection never started
             if (state.selectionRange == null) return
             // Zero out the total distance that being dragged.
@@ -481,7 +487,7 @@
 
                 dragTotalDistance += dragDistance
 
-                selectionRegistrar?.onUpdateSelection(
+                selectionRegistrar?.notifySelectionUpdate(
                     layoutCoordinates = it,
                     startPosition = dragBeginPosition,
                     endPosition = dragBeginPosition + dragTotalDistance
@@ -489,5 +495,13 @@
             }
             return dragDistance
         }
+
+        override fun onStop(velocity: Offset) {
+            selectionRegistrar?.notifySelectionUpdateEnd()
+        }
+
+        override fun onCancel() {
+            selectionRegistrar?.notifySelectionUpdateEnd()
+        }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 37b6ebd..acf8a57 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -17,6 +17,9 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.Interaction
+import androidx.compose.foundation.InteractionState
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.text.selection.TextFieldSelectionHandle
 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
 import androidx.compose.runtime.Composable
@@ -29,11 +32,9 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.gesture.dragGestureFilter
 import androidx.compose.ui.gesture.longPressDragGestureFilter
@@ -58,7 +59,6 @@
 import androidx.compose.ui.selection.SimpleLayout
 import androidx.compose.ui.semantics.copyText
 import androidx.compose.ui.semantics.cutText
-import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.getTextLayoutResult
 import androidx.compose.ui.semantics.imeAction
 import androidx.compose.ui.semantics.onClick
@@ -123,13 +123,16 @@
  * communicating with platform text input service, e.g. software keyboard on Android. Called with
  * [SoftwareKeyboardController] instance which can be used for requesting input show/hide software
  * keyboard.
+ * @param interactionState The [InteractionState] representing the different [Interaction]s
+ * present on this TextField. You can create and pass in your own remembered [InteractionState]
+ * if you want to read the [InteractionState] and customize the appearance / behavior of this
+ * TextField in different [Interaction]s.
  * @param cursorColor Color of the cursor. If [Color.Unspecified], there will be no cursor drawn
  * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
  * text will be positioned as if there was unlimited horizontal space.
  */
 @Composable
 @OptIn(
-    ExperimentalFocus::class,
     ExperimentalTextApi::class,
     MouseTemporaryApi::class
 )
@@ -143,6 +146,7 @@
     visualTransformation: VisualTransformation = VisualTransformation.None,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     onTextInputStarted: (SoftwareKeyboardController) -> Unit = {},
+    interactionState: InteractionState? = null,
     cursorColor: Color = Color.Unspecified,
     softWrap: Boolean = true,
     imeOptions: ImeOptions = ImeOptions.Default
@@ -207,9 +211,9 @@
     manager.hapticFeedBack = AmbientHapticFeedback.current
     manager.focusRequester = focusRequester
 
-    val focusObserver = Modifier.focusObserver {
+    val onFocusChanged = Modifier.onFocusChanged {
         if (state.hasFocus == it.isFocused) {
-            return@focusObserver
+            return@onFocusChanged
         }
 
         state.hasFocus = it.isFocused
@@ -269,6 +273,7 @@
     }
 
     val dragPositionGestureModifier = Modifier.dragPositionGestureFilter(
+        interactionState = interactionState,
         onPress = {
             if (state.hasFocus) {
                 state.selectionIsOn = false
@@ -341,10 +346,10 @@
     }
 
     val semanticsModifier = Modifier.semantics {
+        // focused semantics are handled by Modifier.focusable()
         this.imeAction = imeOptions.imeAction
         this.text = AnnotatedString(value.text)
         this.textSelectionRange = value.selection
-        this.focused = state.hasFocus
         getTextLayoutResult {
             if (state.layoutResult != null) {
                 it.add(state.layoutResult!!)
@@ -411,7 +416,7 @@
 
     val modifiers = modifier
         .focusRequester(focusRequester)
-        .then(focusObserver)
+        .then(onFocusChanged)
         .then(cursorModifier)
         .then(pointerModifier)
         .then(drawModifier)
@@ -419,7 +424,7 @@
         .then(semanticsModifier)
         .textFieldMinSize(textStyle)
         .textFieldKeyboardModifier(manager)
-        .focus()
+        .focusable(interactionState = interactionState)
 
     SimpleLayout(modifiers) {
         Layout(emptyContent()) { _, constraints ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
index 626a730..e22c3ab 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
@@ -18,11 +18,10 @@
 
 import androidx.compose.animation.core.AnimatedFloat
 import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationConstants
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.repeatable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -103,8 +102,7 @@
 }
 
 private val cursorAnimationSpec: AnimationSpec<Float>
-    get() = repeatable(
-        iterations = AnimationConstants.Infinite,
+    get() = infiniteRepeatable(
         animation = keyframes {
             durationMillis = 1000
             1f at 0
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt
index 057acb39..560431f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDragGestureFilter.kt
@@ -16,6 +16,9 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.Interaction
+import androidx.compose.foundation.InteractionState
+import androidx.compose.runtime.onDispose
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
@@ -30,19 +33,28 @@
 @Suppress("ModifierInspectorInfo")
 internal fun Modifier.dragPositionGestureFilter(
     onPress: (Offset) -> Unit,
-    onRelease: (Offset) -> Unit
+    onRelease: (Offset) -> Unit,
+    interactionState: InteractionState?,
 ): Modifier = composed {
     val tracker = remember { DragEventTracker() }
     // TODO(shepshapard): PressIndicator doesn't seem to be the right thing to use here.  It
     //  actually may be functionally correct, but might mostly suggest that it should not
     //  actually be called PressIndicator, but instead something else.
+    onDispose {
+        interactionState?.removeInteraction(Interaction.Pressed)
+    }
     pressIndicatorGestureFilter(
         onStart = {
+            interactionState?.addInteraction(Interaction.Pressed, it)
             tracker.init(it)
             onPress(it)
         },
         onStop = {
+            interactionState?.removeInteraction(Interaction.Pressed)
             onRelease(tracker.getPosition())
+        },
+        onCancel = {
+            interactionState?.removeInteraction(Interaction.Pressed)
         }
     )
         .dragGestureFilter(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
index 905cd82..41b50c6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.gestures.rememberScrollableController
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.runtime.Stable
@@ -38,6 +39,7 @@
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.text.input.TransformedText
 import androidx.compose.ui.text.input.VisualTransformation
@@ -52,17 +54,15 @@
     scrollerPosition: TextFieldScrollerPosition,
     textFieldValue: TextFieldValue,
     visualTransformation: VisualTransformation,
+    interactionState: InteractionState,
     textLayoutResult: Ref<TextLayoutResult?>
 ) = composed(
     factory = {
         // do not reverse direction only in case of RTL in horizontal orientation
         val rtl = AmbientLayoutDirection.current == LayoutDirection.Rtl
         val reverseDirection = orientation == Orientation.Vertical || !rtl
-        val scroll = Modifier.scrollable(
-            orientation = orientation,
-            canScroll = { scrollerPosition.maximum != 0f },
-            reverseDirection = reverseDirection,
-            controller = rememberScrollableController { delta ->
+        val controller =
+            rememberScrollableController(interactionState = interactionState) { delta ->
                 val newOffset = scrollerPosition.offset + delta
                 val consumedDelta = when {
                     newOffset > scrollerPosition.maximum ->
@@ -73,8 +73,16 @@
                 scrollerPosition.offset += consumedDelta
                 consumedDelta
             }
+        val scroll = Modifier.scrollable(
+            orientation = orientation,
+            canScroll = { scrollerPosition.maximum != 0f },
+            reverseDirection = reverseDirection,
+            controller = controller
         )
 
+        val cursorOffset = scrollerPosition.getOffsetToFollow(textFieldValue.selection)
+        scrollerPosition.previousSelection = textFieldValue.selection
+
         val transformedText = visualTransformation.filter(
             AnnotatedString(textFieldValue.text)
         )
@@ -82,14 +90,14 @@
             Orientation.Vertical ->
                 VerticalScrollLayoutModifier(
                     scrollerPosition,
-                    textFieldValue,
+                    cursorOffset,
                     transformedText,
                     textLayoutResult
                 )
             Orientation.Horizontal ->
                 HorizontalScrollLayoutModifier(
                     scrollerPosition,
-                    textFieldValue,
+                    cursorOffset,
                     transformedText,
                     textLayoutResult
                 )
@@ -102,13 +110,14 @@
         properties["scrollerPosition"] = scrollerPosition
         properties["textFieldValue"] = textFieldValue
         properties["visualTransformation"] = visualTransformation
+        properties["interactionState"] = interactionState
         properties["textLayoutResult"] = textLayoutResult
     }
 )
 
 private data class VerticalScrollLayoutModifier(
     val scrollerPosition: TextFieldScrollerPosition,
-    val textFieldValue: TextFieldValue,
+    val cursorOffset: Int,
     val transformedText: TransformedText,
     val textLayoutResult: Ref<TextLayoutResult?>
 ) : LayoutModifier {
@@ -122,9 +131,9 @@
 
         return layout(placeable.width, height) {
             val cursorRect = getCursorRectInScroller(
-                textFieldValue = textFieldValue,
+                cursorOffset = cursorOffset,
                 transformedText = transformedText,
-                textLayoutResult = textLayoutResult,
+                textLayoutResult = textLayoutResult.value,
                 rtl = false,
                 textFieldWidth = placeable.width
             )
@@ -144,7 +153,7 @@
 
 private data class HorizontalScrollLayoutModifier(
     val scrollerPosition: TextFieldScrollerPosition,
-    val textFieldValue: TextFieldValue,
+    val cursorOffset: Int,
     val transformedText: TransformedText,
     val textLayoutResult: Ref<TextLayoutResult?>
 ) : LayoutModifier {
@@ -158,9 +167,9 @@
 
         return layout(width, placeable.height) {
             val cursorRect = getCursorRectInScroller(
-                textFieldValue = textFieldValue,
+                cursorOffset = cursorOffset,
                 transformedText = transformedText,
-                textLayoutResult = textLayoutResult,
+                textLayoutResult = textLayoutResult.value,
                 rtl = layoutDirection == LayoutDirection.Rtl,
                 textFieldWidth = placeable.width
             )
@@ -179,14 +188,14 @@
 }
 
 private fun Density.getCursorRectInScroller(
-    textFieldValue: TextFieldValue,
+    cursorOffset: Int,
     transformedText: TransformedText,
-    textLayoutResult: Ref<TextLayoutResult?>,
+    textLayoutResult: TextLayoutResult?,
     rtl: Boolean,
     textFieldWidth: Int
 ): Rect {
-    val cursorRect = textLayoutResult.value?.getCursorRect(
-        transformedText.offsetMap.originalToTransformed(textFieldValue.selection.min)
+    val cursorRect = textLayoutResult?.getCursorRect(
+        transformedText.offsetMap.originalToTransformed(cursorOffset)
     ) ?: Rect.Zero
     val thickness = DefaultCursorThickness.toIntPx()
 
@@ -226,6 +235,12 @@
      */
     private var previousCursorRect: Rect = Rect.Zero
 
+    /**
+     * Keeps the previous selection data in TextFieldValue in order to identify what has changed
+     * in the new selection, and decide which selection offset (start, end) to follow.
+     */
+    var previousSelection: TextRange = TextRange.Zero
+
     fun update(
         orientation: Orientation,
         cursorRect: Rect,
@@ -257,6 +272,14 @@
         }
     }
 
+    fun getOffsetToFollow(selection: TextRange): Int {
+        return when {
+            selection.start != previousSelection.start -> selection.start
+            selection.end != previousSelection.end -> selection.end
+            else -> selection.min
+        }
+    }
+
     companion object {
         val Saver = Saver<TextFieldScrollerPosition, Float>(
             save = { it.offset },
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index eaa5cc2..f908eb0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -22,10 +22,12 @@
 import androidx.compose.ui.selection.Selectable
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
 import kotlin.math.max
 
+@OptIn(ExperimentalTextApi::class)
 internal class MultiWidgetSelectionDelegate(
     private val selectionRangeUpdate: (TextRange?) -> Unit,
     private val coordinatesCallback: () -> LayoutCoordinates?,
@@ -123,6 +125,7 @@
  *
  * @return [Selection] of the current composable, or null if the composable is not selected.
  */
+@OptIn(ExperimentalTextApi::class)
 internal fun getTextSelectionInfo(
     textLayoutResult: TextLayoutResult,
     selectionCoordinates: Pair<Offset, Offset>,
@@ -206,6 +209,7 @@
  *
  * @return [Selection] of the current composable, or null if the composable is not selected.
  */
+@OptIn(ExperimentalTextApi::class)
 private fun getRefinedSelectionInfo(
     rawStartOffset: Int,
     rawEndOffset: Int,
@@ -282,6 +286,7 @@
  *
  * @return an assembled object of [Selection] using the offered selection info.
  */
+@OptIn(ExperimentalTextApi::class)
 private fun getAssembledSelectionInfo(
     startOffset: Int,
     endOffset: Int,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 67e7bb8..78c50d0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -20,7 +20,6 @@
 import androidx.compose.foundation.text.TextFieldState
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -52,10 +51,7 @@
 /**
  * A bridge class between user interaction to the text field selection.
  */
-@OptIn(
-    InternalTextApi::class,
-    ExperimentalFocus::class
-)
+@OptIn(InternalTextApi::class)
 internal class TextFieldSelectionManager() {
 
     /**
@@ -406,20 +402,13 @@
     }
 
     internal fun getHandlePosition(isStartHandle: Boolean): Offset {
-        return if (isStartHandle)
-            getSelectionHandleCoordinates(
-                textLayoutResult = state?.layoutResult!!,
-                offset = offsetMap.originalToTransformed(value.selection.start),
-                isStart = true,
-                areHandlesCrossed = value.selection.reversed
-            )
-        else
-            getSelectionHandleCoordinates(
-                textLayoutResult = state?.layoutResult!!,
-                offset = offsetMap.originalToTransformed(value.selection.end),
-                isStart = false,
-                areHandlesCrossed = value.selection.reversed
-            )
+        val offset = if (isStartHandle) value.selection.start else value.selection.end
+        return getSelectionHandleCoordinates(
+            textLayoutResult = state?.layoutResult!!,
+            offset = offsetMap.originalToTransformed(offset),
+            isStart = isStartHandle,
+            areHandlesCrossed = value.selection.reversed
+        )
     }
 
     /**
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
index e57a8bce..0384857 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
@@ -42,7 +42,6 @@
 import androidx.compose.ui.input.pointer.pointerMoveFilter
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.MeasuringIntrinsicsMeasureBlocks
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
@@ -89,7 +88,7 @@
 
 /**
  * Vertical scrollbar that can be attached to some scrollable
- * component (ScrollableColumn, LazyColumnFor) and share common state with it.
+ * component (ScrollableColumn, LazyColumn) and share common state with it.
  *
  * Can be placed independently.
  *
@@ -129,7 +128,7 @@
 
 /**
  * Horizontal scrollbar that can be attached to some scrollable
- * component (ScrollableRow, LazyRowFor) and share common state with it.
+ * component (ScrollableRow, LazyRow) and share common state with it.
  *
  * Can be placed independently.
  *
@@ -169,7 +168,6 @@
 
 // TODO(demin): do we need to stop dragging if cursor is beyond constraints?
 // TODO(demin): add Interaction.Hovered to interactionState
-@OptIn(ExperimentalLayoutNodeApi::class)
 @Composable
 private fun Scrollbar(
     adapter: ScrollbarAdapter,
@@ -451,7 +449,7 @@
     val containerSize: Int,
     val minHeight: Float
 ) {
-    private val contentSize = adapter.maxScrollOffset(containerSize) + containerSize
+    private val contentSize get() = adapter.maxScrollOffset(containerSize) + containerSize
     private val visiblePart get() = containerSize.toFloat() / contentSize
 
     val size
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt
index 6b6caac..d5e4a44 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt
@@ -19,7 +19,6 @@
 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.plus
 import androidx.compose.ui.input.key.shortcuts
@@ -40,7 +39,6 @@
 
 private val selectAllKeySet by lazy { modifier + Key.A }
 
-@OptIn(ExperimentalKeyInput::class)
 internal actual fun Modifier.textFieldKeyboardModifier(
     manager: TextFieldSelectionManager
 ): Modifier = composed {
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 efd7cab..c2f7983f 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
@@ -17,15 +17,17 @@
 package androidx.compose.foundation
 
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
@@ -51,6 +53,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.jetbrains.skija.Surface
 import org.junit.Assert.assertEquals
 import org.junit.Ignore
@@ -223,6 +226,35 @@
         }
     }
 
+    @Test(timeout = 3000)
+    fun `dynamically change content then drag slider to the end`() {
+        runBlocking(Dispatchers.Main) {
+            val isContentVisible = mutableStateOf(false)
+            rule.setContent {
+                TestBox(
+                    size = 100.dp,
+                    scrollbarWidth = 10.dp
+                ) {
+                    if (isContentVisible.value) {
+                        repeat(10) {
+                            Box(Modifier.size(20.dp).testTag("box$it"))
+                        }
+                    }
+                }
+            }
+            onFrame()
+
+            isContentVisible.value = true
+            onFrame()
+
+            rule.onNodeWithTag("scrollbar").performGesture {
+                swipe(start = Offset(0f, 25f), end = Offset(0f, 500f))
+            }
+            onFrame()
+            rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
+        }
+    }
+
     @Suppress("SameParameterValue")
     @OptIn(ExperimentalFoundationApi::class)
     @Test(timeout = 3000)
@@ -327,6 +359,8 @@
 
     // TODO(demin): move to DesktopComposeTestRule?
     private suspend fun onFrame() {
+        // TODO(demin): probably we don't need `yield` after we fix https://github.com/JetBrains/compose-jb/issues/137
+        yield()
         (rule as DesktopComposeTestRule).owners?.onFrame(canvas, 100, 100, 0)
         rule.awaitIdle()
     }
@@ -366,6 +400,31 @@
         }
     }
 
+    @Composable
+    private fun TestBox(
+        size: Dp,
+        scrollbarWidth: Dp,
+        scrollableContent: @Composable ColumnScope.() -> Unit
+    ) = withTestEnvironment {
+        Box(Modifier.size(size)) {
+            val state = rememberScrollState()
+
+            ScrollableColumn(
+                Modifier.fillMaxSize().testTag("column"),
+                state,
+                content = scrollableContent
+            )
+
+            VerticalScrollbar(
+                adapter = rememberScrollbarAdapter(state),
+                modifier = Modifier
+                    .width(scrollbarWidth)
+                    .fillMaxHeight()
+                    .testTag("scrollbar")
+            )
+        }
+    }
+
     @Suppress("SameParameterValue")
     @OptIn(ExperimentalFoundationApi::class)
     @Composable
@@ -377,12 +436,13 @@
         scrollbarWidth: Dp,
     ) = withTestEnvironment {
         Box(Modifier.size(size)) {
-            LazyColumnFor(
-                (0 until childCount).toList(),
+            LazyColumn(
                 Modifier.fillMaxSize().testTag("column"),
                 state
             ) {
-                Box(Modifier.size(childSize).testTag("box$it"))
+                items((0 until childCount).toList()) {
+                    Box(Modifier.size(childSize).testTag("box$it"))
+                }
             }
 
             VerticalScrollbar(
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
index f1130b2..a56b6f41 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
@@ -14,12 +14,9 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.foundation.gestures
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.anyPositionChangeConsumed
 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
 import androidx.compose.ui.input.pointer.consumeAllChanges
@@ -34,7 +31,6 @@
 import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalPointerInput::class)
 class DragGestureDetectorTest(dragType: GestureType) {
     enum class GestureType {
         VerticalDrag,
@@ -57,6 +53,7 @@
     private var gestureEnded = false
     private var gestureCanceled = false
     private var consumePositiveOnly = false
+    private var sloppyDetector = false
 
     private val DragTouchSlopUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
         detectDragGestures(
@@ -100,7 +97,7 @@
 
     private val AwaitVerticalDragUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
         forEachGesture {
-            handlePointerInput {
+            awaitPointerEventScope {
                 val down = awaitFirstDown()
                 val slopChange = awaitVerticalTouchSlopOrCancellation(down.id) { change, overSlop ->
                     if (change.positionChange().y > 0f || !consumePositiveOnly) {
@@ -109,8 +106,8 @@
                         change.consumePositionChange(0f, change.positionChange().y)
                     }
                 }
-                if (slopChange != null) {
-                    var pointer = slopChange.id
+                if (slopChange != null || sloppyDetector) {
+                    var pointer = if (sloppyDetector) down.id else slopChange!!.id
                     do {
                         val change = awaitVerticalDragOrCancellation(pointer)
                         if (change == null) {
@@ -131,7 +128,7 @@
 
     private val AwaitHorizontalDragUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
         forEachGesture {
-            handlePointerInput {
+            awaitPointerEventScope {
                 val down = awaitFirstDown()
                 val slopChange =
                     awaitHorizontalTouchSlopOrCancellation(down.id) { change, overSlop ->
@@ -141,8 +138,8 @@
                             change.consumePositionChange(change.positionChange().x, 0f)
                         }
                     }
-                if (slopChange != null) {
-                    var pointer = slopChange.id
+                if (slopChange != null || sloppyDetector) {
+                    var pointer = if (sloppyDetector) down.id else slopChange!!.id
                     do {
                         val change = awaitHorizontalDragOrCancellation(pointer)
                         if (change == null) {
@@ -163,7 +160,7 @@
 
     private val AwaitDragUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
         forEachGesture {
-            handlePointerInput {
+            awaitPointerEventScope {
                 val down = awaitFirstDown()
                 val slopChange = awaitTouchSlopOrCancellation(down.id) { change, overSlop ->
                     val positionChange = change.positionChange()
@@ -173,8 +170,8 @@
                         change.consumeAllChanges()
                     }
                 }
-                if (slopChange != null) {
-                    var pointer = slopChange.id
+                if (slopChange != null || sloppyDetector) {
+                    var pointer = if (sloppyDetector) down.id else slopChange!!.id
                     do {
                         val change = awaitDragOrCancellation(pointer)
                         if (change == null) {
@@ -224,6 +221,13 @@
         else -> false
     }
 
+    private val supportsSloppyGesture = when (dragType) {
+        GestureType.AwaitVerticalDragOrCancel,
+        GestureType.AwaitHorizontalDragOrCancel,
+        GestureType.AwaitDragOrCancel -> true
+        else -> false
+    }
+
     @Before
     fun setup() {
         dragDistance = 0f
@@ -353,75 +357,6 @@
     }
 
     /**
-     * When this drag direction is less than the other drag direction, it should wait
-     * before locking the orientation.
-     */
-    @Test
-    fun dragLockedWithLowPriority() = util.executeInComposition {
-        if (!twoAxisDrag) {
-            down().moveBy(
-                dragMotion + (crossDragMotion * 2f),
-                final = {
-                    // The other direction should have priority, but it should consume the
-                    // in-direction position change
-                    assertEquals(dragMotion, consumed.positionChange)
-
-                    // but it shouldn't have called the callback, yet
-                    assertFalse(dragged)
-                }
-            )
-                .up()
-            assertTrue(dragged)
-            assertTrue(gestureEnded)
-            assertFalse(gestureCanceled)
-            assertEquals(0f, dragDistance)
-        }
-    }
-
-    /**
-     * When this drag direction is less than the other drag direction, it should wait
-     * before locking the orientation. When the other direction locks, it should not drag.
-     */
-    @Test
-    fun dragLockFailWithLowPriority() = util.executeInComposition {
-        if (!twoAxisDrag) {
-            down().moveBy(
-                dragMotion + (crossDragMotion * 2f),
-                final = {
-                    consumeAllChanges()
-                }
-            )
-                .up()
-            assertFalse(dragged)
-            assertFalse(gestureEnded)
-            assertFalse(gestureCanceled)
-        }
-    }
-
-    /**
-     * When this drag direction is less than the other drag direction, it should wait
-     * before locking the orientation. When the other direction locks, it should not drag.
-     */
-    @Test
-    fun dragLockFailNested() = util.executeInComposition {
-        if (!twoAxisDrag) {
-            down().moveBy(
-                dragMotion + (crossDragMotion * 2f),
-                final = {
-                    assertEquals(crossDragMotion * 2f, positionChange())
-                    consumeAllChanges()
-                }
-            ).also {
-                assertEquals(dragMotion, it.positionChange())
-            }
-                .up()
-            assertFalse(dragged)
-            assertFalse(gestureEnded)
-            assertFalse(gestureCanceled)
-        }
-    }
-
-    /**
      * When a drag is not consumed, it should lead to the touch slop being reset. This is
      * important when you drag your finger to
      */
@@ -440,4 +375,27 @@
             consumePositiveOnly = false
         }
     }
+
+    /**
+     * When gesture detectors use the wrong pointer for the drag, it should just not
+     * detect the touch.
+     */
+    @Test
+    fun pointerUpTooQuickly() = util.executeInComposition {
+        if (supportsSloppyGesture) {
+            try {
+                sloppyDetector = true
+
+                val finger1 = down()
+                val finger2 = down()
+                finger1.up()
+                finger2.moveBy(dragMotion).up()
+
+                // The sloppy detector doesn't know to look at finger2
+                assertTrue(gestureCanceled)
+            } finally {
+                sloppyDetector = false
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt
index de07073..aa3caaa 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt
@@ -14,12 +14,9 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.foundation.gestures
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.anyPositionChangeConsumed
 import androidx.compose.ui.input.pointer.consumeAllChanges
 import org.junit.Assert.assertEquals
@@ -31,13 +28,14 @@
 import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalPointerInput::class)
 class MultitouchGestureDetectorTest(val panZoomLock: Boolean) {
     companion object {
         @JvmStatic
         @Parameterized.Parameters
         fun parameters() = arrayOf(false, true)
     }
+
+    private var centroid = Offset.Zero
     private var panned = false
     private var panAmount = Offset.Zero
     private var rotated = false
@@ -46,21 +44,21 @@
     private var zoomAmount = 1f
 
     private val util = SuspendingGestureTestUtil {
-        detectMultitouchGestures(
-            panZoomLock = panZoomLock,
-            onRotate = {
+        detectMultitouchGestures(panZoomLock = panZoomLock) { c, pan, gestureZoom, gestureAngle ->
+            centroid = c
+            if (gestureAngle != 0f) {
                 rotated = true
-                rotateAmount += it
-            },
-            onZoom = {
-                zoomed = true
-                zoomAmount *= it
-            },
-            onPan = {
-                panned = true
-                panAmount += it
+                rotateAmount += gestureAngle
             }
-        )
+            if (gestureZoom != 1f) {
+                zoomed = true
+                zoomAmount *= gestureZoom
+            }
+            if (pan != Offset.Zero) {
+                panned = true
+                panAmount += pan
+            }
+        }
     }
 
     @Before
@@ -91,6 +89,8 @@
         val move2 = move1.moveBy(Offset(0.1f, 0.1f))
         assertTrue(move2.anyPositionChangeConsumed())
 
+        assertEquals(17.7f, centroid.x, 0.1f)
+        assertEquals(17.7f, centroid.y, 0.1f)
         assertTrue(panned)
         assertFalse(zoomed)
         assertFalse(rotated)
@@ -129,6 +129,8 @@
         // Now we've averaged enough movement
         assertTrue(moveB1.anyPositionChangeConsumed())
 
+        assertEquals((5f + 25f + 12.8f) / 2f, centroid.x, 0.1f)
+        assertEquals((5f + 25f + 12.8f) / 2f, centroid.y, 0.1f)
         assertTrue(panned)
         assertTrue(zoomed)
         assertFalse(rotated)
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
index 1114272..b544ab1 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
@@ -23,24 +23,11 @@
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.Recomposer
-import androidx.compose.runtime.SlotTable
 import androidx.compose.runtime.currentComposer
 import androidx.compose.runtime.dispatch.MonotonicFrameClock
 import androidx.compose.runtime.withRunningRecomposer
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.autofill.Autofill
-import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
-import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
-import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.pointer.ConsumedData
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
@@ -50,33 +37,15 @@
 import androidx.compose.ui.input.pointer.PointerInputFilter
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.materialize
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.InternalCoreApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.OwnedLayer
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.node.OwnerSnapshotObserver
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientViewConfiguration
-import androidx.compose.ui.platform.ClipboardManager
-import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.ViewConfiguration
-import androidx.compose.ui.semantics.SemanticsOwner
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.input.TextInputService
-import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Duration
-import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Uptime
 import androidx.compose.ui.unit.milliseconds
-import androidx.compose.ui.platform.WindowManager
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.test.runBlockingTest
@@ -88,7 +57,6 @@
  * [gestureDetector]. The [width] and [height] of the LayoutNode may
  * be provided.
  */
-@OptIn(ExperimentalPointerInput::class)
 internal class SuspendingGestureTestUtil(
     val width: Int = 10,
     val height: Int = 10,
@@ -137,7 +105,6 @@
                     pointerInputFilter = currentComposer
                         .materialize(Modifier.pointerInput(gestureDetector)) as
                         PointerInputFilter
-                    LayoutNode(0, 0, width, height, pointerInputFilter!! as Modifier)
                 }
             }
             yield()
@@ -334,7 +301,6 @@
         block: @Composable () -> Unit
     ): Composer<Unit> {
         return Composer(
-            SlotTable(),
             EmptyApplier(),
             recomposer
         ).apply {
@@ -344,28 +310,10 @@
                 fn(this, 0)
             }
             applyChanges()
-            slotTable.verifyWellFormed()
+            verifyConsistent()
         }
     }
 
-    @Suppress("SameParameterValue")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    private fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
-        LayoutNode().apply {
-            this.modifier = modifier
-            measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks("not supported") {
-                override fun measure(
-                    measureScope: MeasureScope,
-                    measurables: List<Measurable>,
-                    constraints: Constraints
-                ): MeasureResult =
-                    measureScope.layout(x2 - x, y2 - y) {}
-            }
-            attach(MockOwner())
-            measure(Constraints.fixed(x2 - x, y2 - y))
-            place(x, y)
-        }
-
     internal class TestFrameClock : MonotonicFrameClock {
 
         private val frameCh = Channel<Long>()
@@ -379,136 +327,15 @@
             onFrame(frameCh.receive())
     }
 
-    @OptIn(
-        ExperimentalFocus::class,
-        ExperimentalLayoutNodeApi::class,
-        InternalCoreApi::class
-    )
-    private class MockOwner(
-        val position: IntOffset = IntOffset.Zero,
-        override val root: LayoutNode = LayoutNode()
-    ) : Owner {
-        val onRequestMeasureParams = mutableListOf<LayoutNode>()
-        val onAttachParams = mutableListOf<LayoutNode>()
-        val onDetachParams = mutableListOf<LayoutNode>()
-
-        override val hapticFeedBack: HapticFeedback
-            get() = TODO("Not yet implemented")
-        override val clipboardManager: ClipboardManager
-            get() = TODO("Not yet implemented")
-        override val textToolbar: TextToolbar
-            get() = TODO("Not yet implemented")
-        override val autofillTree: AutofillTree
-            get() = TODO("Not yet implemented")
-        override val autofill: Autofill?
-            get() = TODO("Not yet implemented")
-        override val density: Density
-            get() = Density(1f)
-        override val semanticsOwner: SemanticsOwner
-            get() = TODO("Not yet implemented")
-        override val textInputService: TextInputService
-            get() = TODO("Not yet implemented")
-        override val focusManager: FocusManager
-            get() = TODO("Not yet implemented")
-        override val windowManager: WindowManager
-            get() = TODO("Not yet implemented")
-        override val fontLoader: Font.ResourceLoader
-            get() = TODO("Not yet implemented")
-        override val layoutDirection: LayoutDirection
-            get() = LayoutDirection.Ltr
-        override var showLayoutBounds: Boolean = false
-        override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
-
-        override fun onRequestMeasure(layoutNode: LayoutNode) {
-            onRequestMeasureParams += layoutNode
-        }
-
-        override fun onRequestRelayout(layoutNode: LayoutNode) {
-        }
-
-        override val hasPendingMeasureOrLayout = false
-
-        override fun onAttach(node: LayoutNode) {
-            onAttachParams += node
-        }
-
-        override fun onDetach(node: LayoutNode) {
-            onDetachParams += node
-        }
-
-        override fun calculatePosition(): IntOffset = position
-
-        override fun requestFocus(): Boolean = false
-
-        @ExperimentalKeyInput
-        override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
-
-        override fun measureAndLayout() {
-        }
-
-        override fun createLayer(
-            drawBlock: (Canvas) -> Unit,
-            invalidateParentLayer: () -> Unit
-        ): OwnedLayer {
-            return object : OwnedLayer {
-                override val layerId: Long
-                    get() = 0
-
-                override fun updateLayerProperties(
-                    scaleX: Float,
-                    scaleY: Float,
-                    alpha: Float,
-                    translationX: Float,
-                    translationY: Float,
-                    shadowElevation: Float,
-                    rotationX: Float,
-                    rotationY: Float,
-                    rotationZ: Float,
-                    cameraDistance: Float,
-                    transformOrigin: TransformOrigin,
-                    shape: Shape,
-                    clip: Boolean
-                ) {
-                }
-
-                override fun move(position: IntOffset) {
-                }
-
-                override fun resize(size: IntSize) {
-                }
-
-                override fun drawLayer(canvas: Canvas) {
-                    drawBlock(canvas)
-                }
-
-                override fun updateDisplayList() {
-                }
-
-                override fun invalidate() {
-                }
-
-                override fun destroy() {
-                }
-
-                override fun getMatrix(matrix: Matrix) {
-                }
-            }
-        }
-
-        override fun onSemanticsChange() {
-        }
-
-        override val measureIteration: Long = 0
-        override val viewConfiguration: ViewConfiguration
-            get() = TestViewConfiguration()
-    }
-
     @OptIn(ExperimentalComposeApi::class)
     class EmptyApplier : Applier<Unit> {
         override val current: Unit = Unit
         override fun down(node: Unit) {}
         override fun up() {}
-        override fun insert(index: Int, instance: Unit) {
+        override fun insertTopDown(index: Int, instance: Unit) {
+            error("Unexpected")
+        }
+        override fun insertBottomUp(index: Int, instance: Unit) {
             error("Unexpected")
         }
         override fun remove(index: Int, count: Int) {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
index a5740b8..5484742 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -14,12 +14,9 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.foundation.gestures
 
 import androidx.compose.ui.gesture.DoubleTapTimeout
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.gesture.LongPressTimeout
 import androidx.compose.ui.input.pointer.consumeDownChange
 import androidx.compose.ui.input.pointer.consumePositionChange
@@ -34,7 +31,6 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-@OptIn(ExperimentalPointerInput::class)
 class TapGestureDetectorTest {
     private var pressed = false
     private var released = false
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/lazy/IntervalListTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/lazy/IntervalListTest.kt
new file mode 100644
index 0000000..e3128b1
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/lazy/IntervalListTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class IntervalListTest {
+
+    private val intervalList = IntervalList<Int>()
+
+    @Test
+    fun addOneItem_searchInterval() {
+        intervalList.add(1, 10)
+
+        val foundInterval = intervalList.intervalForIndex(0)
+
+        assertThat(foundInterval.content).isEqualTo(10)
+        assertThat(foundInterval.size).isEqualTo(1)
+        assertThat(foundInterval.startIndex).isEqualTo(0)
+    }
+
+    @Test
+    fun addOneItem_searchIndexOutOfBounds() {
+        intervalList.add(1, 10)
+
+        val wasException: Boolean = try {
+            intervalList.intervalForIndex(2)
+            false
+        } catch (e: IndexOutOfBoundsException) {
+            true
+        }
+
+        assertThat(wasException).isTrue()
+    }
+
+    @Test
+    fun addSingleItems_searchFirstInterval() {
+        addFiveSingleIntervals()
+
+        val foundInterval = intervalList.intervalForIndex(0)
+
+        assertThat(foundInterval.content).isEqualTo(10)
+        assertThat(foundInterval.size).isEqualTo(1)
+        assertThat(foundInterval.startIndex).isEqualTo(0)
+    }
+
+    @Test
+    fun addSingleItems_searchLastInterval() {
+        addFiveSingleIntervals()
+
+        val foundInterval = intervalList.intervalForIndex(4)
+
+        assertThat(foundInterval.content).isEqualTo(50)
+        assertThat(foundInterval.size).isEqualTo(1)
+        assertThat(foundInterval.startIndex).isEqualTo(4)
+    }
+
+    @Test
+    fun addSingleItems_searchMidInterval() {
+        addFiveSingleIntervals()
+
+        val foundInterval = intervalList.intervalForIndex(2)
+
+        assertThat(foundInterval.content).isEqualTo(30)
+        assertThat(foundInterval.size).isEqualTo(1)
+        assertThat(foundInterval.startIndex).isEqualTo(2)
+    }
+
+    @Test
+    fun addVariableItems_searchFirstInterval() {
+        addFiveVariableIntervals()
+
+        val foundInterval = intervalList.intervalForIndex(0)
+
+        assertThat(foundInterval.content).isEqualTo(10)
+        assertThat(foundInterval.size).isEqualTo(3)
+        assertThat(foundInterval.startIndex).isEqualTo(0)
+    }
+
+    @Test
+    fun addVariableItems_searchLastInterval() {
+        addFiveVariableIntervals()
+
+        val foundInterval = intervalList.intervalForIndex(22)
+
+        assertThat(foundInterval.content).isEqualTo(50)
+        assertThat(foundInterval.size).isEqualTo(11)
+        assertThat(foundInterval.startIndex).isEqualTo(12)
+    }
+
+    @Test
+    fun addVariableItems_searchMidInterval() {
+        addFiveVariableIntervals()
+
+        val foundInterval = intervalList.intervalForIndex(6)
+
+        assertThat(foundInterval.content).isEqualTo(30)
+        assertThat(foundInterval.size).isEqualTo(7)
+        assertThat(foundInterval.startIndex).isEqualTo(4)
+    }
+
+    @Test
+    fun addVariableItems_searchSecondInterval() {
+        addFiveVariableIntervals()
+
+        val foundInterval = intervalList.intervalForIndex(3)
+
+        assertThat(foundInterval.content).isEqualTo(20)
+        assertThat(foundInterval.size).isEqualTo(1)
+        assertThat(foundInterval.startIndex).isEqualTo(3)
+    }
+
+    @Test
+    fun addVariableItems_searchIndexOutOfBounds() {
+        addFiveVariableIntervals()
+
+        val wasException: Boolean = try {
+            intervalList.intervalForIndex(23)
+            false
+        } catch (e: IndexOutOfBoundsException) {
+            true
+        }
+
+        assertThat(wasException).isTrue()
+    }
+
+    private fun addFiveSingleIntervals() {
+        intervalList.add(1, 10)
+        intervalList.add(1, 20)
+        intervalList.add(1, 30)
+        intervalList.add(1, 40)
+        intervalList.add(1, 50)
+    }
+
+    private fun addFiveVariableIntervals() {
+        intervalList.add(3, 10)
+        intervalList.add(1, 20)
+        intervalList.add(7, 30)
+        intervalList.add(1, 40)
+        intervalList.add(11, 50)
+    }
+}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 9ea31ce..cd6e838 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.ui.selection.Selectable
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.selection.SelectionRegistrar
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.TextDelegate
 import androidx.compose.ui.text.style.ResolvedTextDirection
@@ -36,7 +37,10 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 class TextSelectionLongPressDragTest {
     private val selectionRegistrar = mock<SelectionRegistrar>()
     private val selectable = mock<Selectable>()
@@ -93,15 +97,14 @@
     }
 
     @Test
-    fun longPressDragObserver_onLongPress_calls_getSelection_change_selection() {
+    fun longPressDragObserver_onLongPress_calls_notifySelectionInitiated() {
         val position = Offset(100f, 100f)
 
         gesture.onLongPress(position)
 
-        verify(selectionRegistrar, times(1)).onUpdateSelection(
+        verify(selectionRegistrar, times(1)).notifySelectionUpdateStart(
             layoutCoordinates = layoutCoordinates,
-            startPosition = position,
-            endPosition = position
+            startPosition = position
         )
     }
 
@@ -127,7 +130,7 @@
 
         // Verify.
         verify(selectionRegistrar, times(1))
-            .onUpdateSelection(
+            .notifySelectionUpdate(
                 layoutCoordinates = layoutCoordinates,
                 startPosition = beginPosition2,
                 endPosition = beginPosition2 + dragDistance2
@@ -135,7 +138,7 @@
     }
 
     @Test
-    fun longPressDragObserver_onDrag_calls_getSelection_change_selection() {
+    fun longPressDragObserver_onDrag_calls_notifySelectionDrag() {
         val dragDistance = Offset(15f, 10f)
         val beginPosition = Offset(30f, 20f)
         gesture.onLongPress(beginPosition)
@@ -146,10 +149,35 @@
 
         assertThat(result).isEqualTo(dragDistance)
         verify(selectionRegistrar, times(1))
-            .onUpdateSelection(
+            .notifySelectionUpdate(
                 layoutCoordinates = layoutCoordinates,
                 startPosition = beginPosition,
                 endPosition = beginPosition + dragDistance
             )
     }
+
+    @Test
+    fun longPressDragObserver_onStop_calls_notifySelectionEnd() {
+        val dragDistance = Offset(15f, 10f)
+        val beginPosition = Offset(30f, 20f)
+        gesture.onLongPress(beginPosition)
+        state.selectionRange = fakeInitialSelection.toTextRange()
+        gesture.onDragStart()
+        gesture.onStop(dragDistance)
+
+        verify(selectionRegistrar, times(1))
+            .notifySelectionUpdateEnd()
+    }
+
+    @Test
+    fun longPressDragObserver_onCancel_calls_notifySelectionEnd() {
+        val beginPosition = Offset(30f, 20f)
+        gesture.onLongPress(beginPosition)
+        state.selectionRange = fakeInitialSelection.toTextRange()
+        gesture.onDragStart()
+        gesture.onCancel()
+
+        verify(selectionRegistrar, times(1))
+            .notifySelectionUpdateEnd()
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index 78d7fff..e39145c 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation.text.selection
 
 import androidx.compose.foundation.text.TextFieldState
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -54,10 +53,7 @@
 import org.mockito.stubbing.Answer
 
 @RunWith(JUnit4::class)
-@OptIn(
-    InternalTextApi::class,
-    ExperimentalFocus::class
-)
+@OptIn(InternalTextApi::class)
 class TextFieldSelectionManagerTest {
     private val text = "Hello World"
     private val density = Density(density = 1f)
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
index 4c14493..8e652ac 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
@@ -24,11 +24,7 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyColumnFor
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
 import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.LazyRowFor
-import androidx.compose.foundation.lazy.LazyRowForIndexed
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.emptyContent
@@ -92,13 +88,9 @@
                 LazyColumnWithItemAndItems,
                 LazyColumnWithItems,
                 LazyColumnWithItemsIndexed,
-                LazyColumnFor,
-                LazyColumnForIndexed,
                 LazyRowWithItemAndItems,
                 LazyRowWithItems,
-                LazyRowWithItemsIndexed,
-                LazyRowFor,
-                LazyRowForIndexed
+                LazyRowWithItemsIndexed
             )
     }
 }
@@ -147,26 +139,6 @@
     }
 }
 
-private val LazyColumnFor = LazyListScrollingTestCase("LazyColumnFor") {
-    LazyColumnFor(items, modifier = Modifier.height(400.dp).fillMaxWidth()) {
-        if (it.index == 0) {
-            RemeasurableItem()
-        } else {
-            RegularItem()
-        }
-    }
-}
-
-private val LazyColumnForIndexed = LazyListScrollingTestCase("LazyColumnForIndexed") {
-    LazyColumnForIndexed(items, modifier = Modifier.height(400.dp).fillMaxWidth()) { index, _ ->
-        if (index == 0) {
-            RemeasurableItem()
-        } else {
-            RegularItem()
-        }
-    }
-}
-
 private val LazyRowWithItemAndItems = LazyListScrollingTestCase("LazyRowWithItemAndItems") {
     LazyRow(modifier = Modifier.width(400.dp).fillMaxHeight()) {
         item {
@@ -202,26 +174,6 @@
     }
 }
 
-private val LazyRowFor = LazyListScrollingTestCase("LazyRowFor") {
-    LazyRowFor(items, modifier = Modifier.width(400.dp).fillMaxHeight()) {
-        if (it.index == 0) {
-            RemeasurableItem()
-        } else {
-            RegularItem()
-        }
-    }
-}
-
-private val LazyRowForIndexed = LazyListScrollingTestCase("LazyRowForIndexed") {
-    LazyRowForIndexed(items, modifier = Modifier.width(400.dp).fillMaxHeight()) { index, _ ->
-        if (index == 0) {
-            RemeasurableItem()
-        } else {
-            RegularItem()
-        }
-    }
-}
-
 // TODO(b/169852102 use existing public constructs instead)
 private fun ComposeBenchmarkRule.toggleStateBenchmarkMeasure(
     caseFactory: () -> ListRemeasureTestCase
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
index 9f9e3dd..7d08b8b 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
@@ -14,29 +14,27 @@
  * limitations under the License.
  */
 
+@file:Suppress("DEPRECATION_ERROR")
+
 package androidx.ui.benchmark.test
 
-import android.view.ViewGroup
 import androidx.activity.ComponentActivity
 import androidx.benchmark.junit4.BenchmarkRule
 import androidx.benchmark.junit4.measureRepeated
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.padding
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.gesture.pressIndicatorGestureFilter
 import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.key.ExperimentalKeyInput
-import androidx.compose.ui.input.key.keyInputFilter
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.layout.TestModifierUpdater
+import androidx.compose.ui.layout.TestModifierUpdaterLayout
 import androidx.compose.ui.layout.layoutId
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AndroidOwner
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
 import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
 import androidx.test.filters.LargeTest
@@ -51,11 +49,10 @@
 import org.junit.runners.model.Statement
 
 /**
- * Benchmark that sets the [LayoutNode.modifier].
+ * Benchmark that sets the LayoutNode.modifier.
  */
 @LargeTest
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalLayoutNodeApi::class)
 class LayoutNodeModifierBenchmark(
     private val numberOfModifiers: Int
 ) {
@@ -70,16 +67,15 @@
 
     var modifiers = emptyList<Modifier>()
     var combinedModifier: Modifier = Modifier
-    lateinit var layoutNode: LayoutNode
+    lateinit var testModifierUpdater: TestModifierUpdater
 
     @Before
-    @OptIn(ExperimentalKeyInput::class)
     fun setup() {
         modifiers = listOf(
             Modifier.padding(10.dp),
             Modifier.drawBehind { },
             Modifier.graphicsLayer(),
-            Modifier.keyInputFilter { _ -> true },
+            Modifier.onKeyEvent { true },
             Modifier.semantics { },
             Modifier.pressIndicatorGestureFilter(),
             Modifier.layoutId("Hello"),
@@ -93,14 +89,11 @@
         }
 
         rule.activityTestRule.runOnUiThread {
-            rule.activityTestRule.activity.setContent { Box(Modifier) }
-        }
-        rule.activityTestRule.runOnUiThread {
-            val composeView = rule.findAndroidOwner()
-            val root = composeView.root
-            check(root.children.size == 1) { "Expecting only a Box" }
-            layoutNode = root.children[0]
-            check(layoutNode.children.isEmpty()) { "Box should be empty" }
+            rule.activityTestRule.activity.setContent {
+                TestModifierUpdaterLayout {
+                    testModifierUpdater = it
+                }
+            }
         }
     }
 
@@ -108,8 +101,8 @@
     fun setAndClearModifiers() {
         rule.activityTestRule.runOnUiThread {
             rule.benchmarkRule.measureRepeated {
-                layoutNode.modifier = combinedModifier
-                layoutNode.modifier = Modifier
+                testModifierUpdater.updateModifier(combinedModifier)
+                testModifierUpdater.updateModifier(Modifier)
             }
         }
     }
@@ -118,11 +111,11 @@
     fun smallModifierChange() {
         rule.activityTestRule.runOnUiThread {
             val altModifier = Modifier.padding(10.dp).then(combinedModifier)
-            layoutNode.modifier = altModifier
+            testModifierUpdater.updateModifier(altModifier)
 
             rule.benchmarkRule.measureRepeated {
-                layoutNode.modifier = combinedModifier
-                layoutNode.modifier = altModifier
+                testModifierUpdater.updateModifier(combinedModifier)
+                testModifierUpdater.updateModifier(altModifier)
             }
         }
     }
@@ -142,10 +135,5 @@
                 .around(activityTestRule)
                 .apply(base, description)
         }
-
-        fun findAndroidOwner(): AndroidOwner {
-            return activityTestRule.activity.findViewById<ViewGroup>(android.R.id.content)
-                .getChildAt(0) as AndroidOwner
-        }
     }
 }
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
index 77859d6..4aee2ae 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
@@ -21,6 +21,7 @@
 import android.view.autofill.AutofillValue
 import androidx.benchmark.junit4.BenchmarkRule
 import androidx.benchmark.junit4.measureRepeated
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.autofill.AutofillNode
 import androidx.compose.ui.autofill.AutofillTree
 import androidx.compose.ui.autofill.AutofillType
@@ -38,6 +39,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 
 @LargeTest
+@OptIn(ExperimentalComposeUiApi::class)
 @RunWith(AndroidJUnit4::class)
 class AndroidAutofillBenchmark {
 
@@ -58,6 +60,7 @@
         }
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
     @Test
     @UiThreadTest
     @SdkSuppress(minSdkVersion = 26)
diff --git a/compose/integration-tests/demos/build.gradle b/compose/integration-tests/demos/build.gradle
index 2f7c2c0..e422582 100644
--- a/compose/integration-tests/demos/build.gradle
+++ b/compose/integration-tests/demos/build.gradle
@@ -32,7 +32,7 @@
     implementation project(":compose:runtime:runtime")
     implementation project(":compose:ui:ui")
 
-    implementation "androidx.preference:preference-ktx:1.1.0"
+    implementation "androidx.preference:preference-ktx:1.1.1"
 
     androidTestImplementation project(":compose:ui:ui-test-junit4")
 
diff --git a/compose/integration-tests/demos/lint-baseline.xml b/compose/integration-tests/demos/lint-baseline.xml
deleted file mode 100644
index 08f1cf1..0000000
--- a/compose/integration-tests/demos/lint-baseline.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
-    <issue
-        id="ObsoleteLintCustomCheck"
-        message="Lint found an issue registry (`androidx.lifecycle.lint.LifecycleRuntimeIssueRegistry`) which is older than the current API level; these checks may not work correctly.&#xA;&#xA;Recompile the checks against the latest version. Custom check API version is 6 (3.6), current lint API level is 8 (4.1)">
-        <location
-            file="../../../../../../../home/jeffrygaston/.gradle/caches/transforms-2/files-2.1/02dcda78691438fc76cdfd012889a28d/lifecycle-runtime-ktx-2.2.0/jars/lint.jar"/>
-    </issue>
-
-</issues>
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
index 4f908ce..5bb3529 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
@@ -36,7 +36,6 @@
     var light: Colors by mutableStateOf(lightColors())
     var dark: Colors by mutableStateOf(darkColors())
 
-    @Composable
     val colors
-        get() = if (isSystemInDarkTheme()) dark else light
+        @Composable get() = if (isSystemInDarkTheme()) dark else light
 }
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
index 72d901a..7f2a927 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
@@ -39,13 +39,12 @@
 import androidx.compose.runtime.onCommit
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
 import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.dp
 
 /**
@@ -101,10 +100,7 @@
  * [BasicTextField] that edits the current [filterText], providing [onFilter] when edited.
  */
 @Composable
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalFoundationApi::class)
 private fun FilterField(
     filterText: String,
     onFilter: (String) -> Unit,
@@ -132,7 +128,7 @@
     onNavigate: (Demo) -> Unit
 ) {
     val primary = MaterialTheme.colors.primary
-    val annotatedString = annotatedString {
+    val annotatedString = buildAnnotatedString {
         val title = demo.title
         var currentIndex = 0
         val pattern = filterText.toRegex(option = RegexOption.IGNORE_CASE)
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
index dcab8ad..07e9583 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
@@ -29,7 +29,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Button
 import androidx.compose.material.Scaffold
 import androidx.compose.material.Surface
@@ -137,8 +137,10 @@
         onSelected: (Artist) -> Unit
     ) {
         Surface(Modifier.fillMaxSize()) {
-            LazyColumnFor(feedItems) { item ->
-                ArtistCard(item, onSelected)
+            LazyColumn {
+                items(feedItems) { item ->
+                    ArtistCard(item, onSelected)
+                }
             }
         }
     }
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt
index e110e4d..635fbdf 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt
@@ -23,7 +23,7 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Button
 import androidx.compose.material.Checkbox
 import androidx.compose.material.Divider
@@ -118,12 +118,14 @@
             Text(header, style = MaterialTheme.typography.h5)
             Divider()
 
-            // LazyColumnFor is the Compose version of a RecyclerView.
-            // The lambda passed is similar to a RecyclerView.ViewHolder.
-            LazyColumnFor(names) { name ->
-                // When an item's [name] updates, the adapter for that item
-                // will recompose. This will not recompose when [header] changes
-                NamePickerItem(name, onNameClicked)
+            // LazyColumn is the Compose version of a RecyclerView.
+            // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
+            LazyColumn {
+                items(names) { name ->
+                    // When an item's [name] updates, the adapter for that item
+                    // will recompose. This will not recompose when [header] changes
+                    NamePickerItem(name, onNameClicked)
+                }
             }
         }
     }
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
index 3661374..25c3edd 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
@@ -21,7 +21,7 @@
 package androidx.compose.integration.docs.preview
 
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants.defaultButtonColors
+import androidx.compose.material.ButtonDefaults.buttonColors
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
@@ -78,7 +78,7 @@
     fun Counter(count: Int, updateCount: (Int) -> Unit) {
         Button(
             onClick = { updateCount(count + 1) },
-            colors = defaultButtonColors(
+            colors = buttonColors(
                 backgroundColor = if (count > 5) Color.Green else Color.White
             )
         ) {
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
index 2c92a77..2dd4e4e 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
@@ -327,7 +327,7 @@
 
 private const val it = 1
 private lateinit var helloViewModel: StateSnippet2.HelloViewModel
-private fun computeTextFormatting(st: String) {}
+private fun computeTextFormatting(st: String): String = ""
 
 private fun ExpandingCard(
     title: String,
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
index a14e06bc..c5f7f64 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
@@ -25,11 +25,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.semantics.AccessibilityAction
 import androidx.compose.ui.semantics.SemanticsPropertyKey
-import androidx.compose.ui.semantics.accessibilityLabel
+import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -47,10 +46,10 @@
 import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.hasText
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onAllNodesWithLabel
+import androidx.compose.ui.test.onAllNodesWithContentDescription
 import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onNodeWithLabel
+import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performClick
@@ -73,7 +72,7 @@
  */
 
 @Composable private fun TestingSnippet1() {
-    MyButton(modifier = Modifier.semantics { accessibilityLabel = "Like button" })
+    MyButton(modifier = Modifier.semantics { contentDescription = "Like button" })
 }
 
 private object TestingSnippet3 {
@@ -142,11 +141,11 @@
 
 @Composable private fun TestingSnippets8() {
     // Check number of matched nodes
-    composeTestRule.onAllNodesWithLabel("Beatle").assertCountEquals(4)
+    composeTestRule.onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
     // At least one matches
-    composeTestRule.onAllNodesWithLabel("Beatle").assertAny(hasTestTag("Drummer"))
+    composeTestRule.onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
     // All of them match
-    composeTestRule.onAllNodesWithLabel("Beatle").assertAll(hasClickAction())
+    composeTestRule.onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
 }
 
 @Composable private fun SemanticsNodeInteraction.TestingSnippets9() {
@@ -202,13 +201,13 @@
 
         @Test
         fun changeTheme_scrollIsPersisted() {
-            composeTestRule.onNodeWithLabel("Continue").performClick()
+            composeTestRule.onNodeWithContentDescription("Continue").performClick()
 
             // Set theme to dark
             themeIsDark.value = true
 
             // Check that we're still on the same page
-            composeTestRule.onNodeWithLabel("Welcome").assertIsDisplayed()
+            composeTestRule.onNodeWithContentDescription("Welcome").assertIsDisplayed()
         }
     }
 }
@@ -228,5 +227,4 @@
 private class MyActivity : ComponentActivity()
 @Composable private fun MyButton(content: @Composable RowScope.() -> Unit) { }
 private lateinit var key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>
-@OptIn(ExperimentalKeyInput::class)
 private lateinit var keyEvent: KeyEvent
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt
index 918997d..95a8359 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt
@@ -26,7 +26,7 @@
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.AmbientContentAlpha
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Colors
 import androidx.compose.material.ContentAlpha
 import androidx.compose.material.MaterialTheme
@@ -179,9 +179,8 @@
 }
 
 private object ThemingSnippet11 {
-    @Composable
     val Colors.snackbarAction: Color
-        get() = if (isLight) Red300 else Red700
+        @Composable get() = if (isLight) Red300 else Red700
 }
 
 @Composable private fun ThemingSnippet12() {
@@ -251,7 +250,7 @@
         content: @Composable RowScope.() -> Unit
     ) {
         Button(
-            colors = ButtonConstants.defaultButtonColors(
+            colors = ButtonDefaults.buttonColors(
                 backgroundColor = MaterialTheme.colors.secondary
             ),
             onClick = onClick,
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
new file mode 100644
index 0000000..666cb1c
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -0,0 +1,39 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("AndroidXUiPlugin")
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
+        }
+    }
+}
+
+dependencies {
+    kotlinPlugin project(":compose:compiler:compiler")
+
+    implementation(KOTLIN_STDLIB)
+    implementation project(":compose:foundation:foundation-layout")
+    implementation project(":compose:material:material")
+    implementation project(":compose:runtime:runtime")
+    implementation project(":compose:ui:ui")
+    implementation project(":compose:ui:ui-tooling")
+}
+
+android.defaultConfig.minSdkVersion 21
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        useIR = true
+    }
+}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..baca019
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="androidx.compose.integration.macrobenchmark.target">
+
+    <application
+        android:label="Jetpack Compose Macrobenchmark Target"
+        android:allowBackup="false"
+        android:supportsRtl="true"
+        android:icon="@mipmap/ic_launcher"
+        tools:ignore="GoogleAppIndexingWarning">
+
+        <!--
+        Activities need to be exported so the macrobenchmark can discover them
+        under the new package visibility changes for Android 11.
+         -->
+        <activity
+            android:name=".TrivialStartupActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.compose.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".LazyColumnActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/ic_launcher-web.png b/compose/integration-tests/macrobenchmark-target/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..88e5f3b
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/ic_launcher-web.png
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyColumnActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyColumnActivity.kt
new file mode 100644
index 0000000..d981bab
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyColumnActivity.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Card
+import androidx.compose.material.Checkbox
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.unit.dp
+
+class LazyColumnActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 1000)
+
+        setContent {
+            LazyColumn(modifier = Modifier.fillMaxWidth()) {
+                items(List(itemCount) { Entry("Item $it") }) {
+                    ListRow(it)
+                }
+            }
+        }
+    }
+
+    companion object {
+        const val EXTRA_ITEM_COUNT = "ITEM_COUNT"
+    }
+}
+
+@Composable
+private fun ListRow(entry: Entry) {
+    Card(modifier = Modifier.padding(8.dp)) {
+        Row {
+            Text(
+                text = entry.contents,
+                modifier = Modifier.padding(16.dp)
+            )
+            Spacer(modifier = Modifier.weight(1f, fill = true))
+            Checkbox(
+                checked = false,
+                onCheckedChange = {},
+                modifier = Modifier.padding(16.dp)
+            )
+        }
+    }
+}
+
+data class Entry(val contents: String)
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/TrivialStartupActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/TrivialStartupActivity.kt
new file mode 100644
index 0000000..49dd58e
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/TrivialStartupActivity.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.compose.material.Text
+import androidx.compose.ui.platform.setContent
+
+class TrivialStartupActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContent {
+            Text("Compose Macrobenchmark Target")
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-v24/ic_launcher_foreground.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..bbbd1ec
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,50 @@
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+    <path
+        android:fillType="evenOdd"
+        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+        android:strokeColor="#00000000"
+        android:strokeWidth="1">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="78.5885"
+                android:endY="90.9159"
+                android:startX="48.7653"
+                android:startY="61.0927"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+        android:strokeColor="#00000000"
+        android:strokeWidth="1" />
+</vector>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_background.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..2aea1e0
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector
+    android:height="108dp"
+    android:width="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#008577"
+          android:pathData="M0,0h108v108h-108z"/>
+    <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+    <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+          android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+</vector>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher.png b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..898f3ed
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher.png b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..64ba76f
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher.png b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e5ed4659
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher.png b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b0907ca
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2c18de9
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark/build.gradle b/compose/integration-tests/macrobenchmark/build.gradle
index 9a6d909a..1cf4c30 100644
--- a/compose/integration-tests/macrobenchmark/build.gradle
+++ b/compose/integration-tests/macrobenchmark/build.gradle
@@ -27,7 +27,10 @@
     id("kotlin-android")
 }
 
-android.defaultConfig.minSdkVersion 28
+android.defaultConfig {
+    minSdkVersion 28
+    testInstrumentationRunnerArgument 'androidx.benchmark.output.enable', 'true'
+}
 
 dependencies {
     androidTestImplementation(project(":benchmark:benchmark-junit4"))
@@ -40,4 +43,4 @@
 
 // Define a task dependency so the app is installed before we run macro benchmarks.
 tasks.getByPath(':compose:integration-tests:macrobenchmark:connectedCheck')
-    .dependsOn(tasks.getByPath(':compose:integration-tests:demos:installRelease'))
+    .dependsOn(tasks.getByPath(':compose:integration-tests:macrobenchmark-target:installRelease'))
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
index df1de06..b86f8ba 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
@@ -19,13 +19,14 @@
 
     <!--
     The Macro Benchmark Sample needs to launch activities in
-    `androidx.compose.integration.demos` APK.
+    `androidx.compose.integration.macrobenchmark.target` APK.
 
     The Macro Benchmark Library uses `PackageManager` to query for activities. This requires
-     the test APK to declare that `androidx.compose.integration.demos` be visible to
+     the test APK to declare that `androidx.compose.integration.macrobenchmark.target` be visible to
      the APK (given Android 11's package visibility rules).
     -->
     <queries>
-        <package android:name="androidx.compose.integration.demos" />
+        <package android:name="androidx.compose.integration.macrobenchmark.target" />
     </queries>
+    <application android:requestLegacyExternalStorage="true"/>
 </manifest>
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
index b7e167e..0fc4cabb 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
@@ -19,6 +19,7 @@
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.MacrobenchmarkConfig
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -35,37 +36,36 @@
 @RunWith(Parameterized::class)
 class ProcessSpeedProfileValidation(
     private val compilationMode: CompilationMode,
-    private val killProcess: Boolean
+    private val startupMode: StartupMode
 ) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
-    fun start() {
-        val config = MacrobenchmarkConfig(
+    fun start() = benchmarkRule.measureStartupRepeated(
+        MacrobenchmarkConfig(
             packageName = PACKAGE_NAME,
             metrics = listOf(StartupTimingMetric()),
             compilationMode = compilationMode,
-            killProcessEachIteration = killProcess,
-            iterations = 10
-        )
-        benchmarkRule.measureRepeated(config) {
-            pressHome()
-            launchPackageAndWait()
-        }
+            iterations = 3
+        ),
+        startupMode
+    ) {
+        pressHome()
+        launchPackageAndWait()
     }
 
     companion object {
-        private const val PACKAGE_NAME = "androidx.compose.integration.demos"
+        private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+        @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
         @JvmStatic
         fun kilProcessParameters(): List<Array<Any>> {
             val compilationModes = listOf(
                 CompilationMode.None,
                 CompilationMode.SpeedProfile(warmupIterations = 3)
             )
-            val processKillOptions = listOf(true, false)
+            val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
             return compilationModes.zip(processKillOptions).map {
                 arrayOf(it.first, it.second)
             }
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
similarity index 63%
copy from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
copy to compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
index 4ce15f7..34cf7f4 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -17,40 +17,34 @@
 package androidx.compose.integration.macrobenchmark
 
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class) // Parameterized to work around timeouts (b/174175784)
-class StartupDemosMacrobenchmark(
-    private val ignored: Boolean
-) {
-
+@RunWith(Parameterized::class)
+class SmallListStartupBenchmark(private val startupMode: StartupMode) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
-    fun compiledColdStartup() = benchmarkRule.measureStartup(
+    fun startup() = benchmarkRule.measureStartup(
         profileCompiled = true,
-        coldLaunch = true
-    )
-
-    @Test
-    fun uncompiledColdStartup() = benchmarkRule.measureStartup(
-        profileCompiled = false,
-        coldLaunch = true
-    )
+        startupMode = startupMode
+    ) {
+        action = "androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY"
+        putExtra("ITEM_COUNT", 5)
+    }
 
     companion object {
+        @Parameterized.Parameters(name = "mode={0}")
         @JvmStatic
-        @Parameterized.Parameters
-        fun startupDemosParameters(): List<Array<Any>> {
-            return listOf(arrayOf(false))
+        fun parameters(): List<Array<Any>> {
+            return listOf(StartupMode.COLD, StartupMode.WARM)
+                .map { arrayOf(it) }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
index 2a9b90d94..4ef10ad4 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
@@ -16,36 +16,40 @@
 
 package androidx.compose.integration.macrobenchmark
 
+import android.content.Intent
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.MacrobenchmarkConfig
 import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.benchmark.macro.MacrobenchmarkScope
+import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
 
+const val TargetPackage = "androidx.compose.integration.macrobenchmark.target"
+
 /**
  * Simplified interface for standardizing e.g. package,
  * compilation types, and iteration count across project
  */
 fun MacrobenchmarkRule.measureStartup(
     profileCompiled: Boolean,
-    coldLaunch: Boolean,
-    setupBlock: MacrobenchmarkScope.() -> Unit = {},
-    measureBlock: MacrobenchmarkScope.() -> Unit = {
-        pressHome()
-        launchPackageAndWait()
-    }
-) = measureRepeated(
+    startupMode: StartupMode,
+    iterations: Int = 5,
+    setupIntent: Intent.() -> Unit = {}
+) = measureStartupRepeated(
     MacrobenchmarkConfig(
-        packageName = "androidx.compose.integration.demos",
+        packageName = TargetPackage,
         metrics = listOf(StartupTimingMetric()),
         compilationMode = if (profileCompiled) {
             CompilationMode.SpeedProfile(warmupIterations = 3)
         } else {
             CompilationMode.None
         },
-        killProcessEachIteration = coldLaunch,
-        iterations = 10
+        iterations = iterations
     ),
-    setupBlock,
-    measureBlock
-)
+    startupMode = startupMode
+) {
+    pressHome()
+    val intent = Intent()
+    intent.setPackage(TargetPackage)
+    setupIntent(intent)
+    launchIntentAndWait(intent)
+}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
similarity index 63%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
index 4ce15f7..0d92fdc 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -17,40 +17,33 @@
 package androidx.compose.integration.macrobenchmark
 
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class) // Parameterized to work around timeouts (b/174175784)
-class StartupDemosMacrobenchmark(
-    private val ignored: Boolean
-) {
-
+@RunWith(Parameterized::class)
+class TrivialStartupBenchmark(private val startupMode: StartupMode) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
-    fun compiledColdStartup() = benchmarkRule.measureStartup(
+    fun startup() = benchmarkRule.measureStartup(
         profileCompiled = true,
-        coldLaunch = true
-    )
-
-    @Test
-    fun uncompiledColdStartup() = benchmarkRule.measureStartup(
-        profileCompiled = false,
-        coldLaunch = true
-    )
+        startupMode = startupMode
+    ) {
+        action = "androidx.compose.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
+    }
 
     companion object {
+        @Parameterized.Parameters(name = "mode={0}")
         @JvmStatic
-        @Parameterized.Parameters
-        fun startupDemosParameters(): List<Array<Any>> {
-            return listOf(arrayOf(false))
+        fun parameters(): List<Array<Any>> {
+            return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
+                .map { arrayOf(it) }
         }
     }
 }
diff --git a/compose/material/material/OWNERS b/compose/material/OWNERS
similarity index 100%
rename from compose/material/material/OWNERS
rename to compose/material/OWNERS
diff --git a/compose/material/material-ripple/api/current.txt b/compose/material/material-ripple/api/current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+  @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+    method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+  }
+
+  public final class RippleAnimationKt {
+  }
+
+  public final class RippleKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
+    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+    field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+  }
+
+  public static final class RippleTheme.Companion {
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+  }
+
+  public final class RippleThemeKt {
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+  }
+
+}
+
diff --git a/compose/material/material-ripple/api/public_plus_experimental_current.txt b/compose/material/material-ripple/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/public_plus_experimental_current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+  @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+    method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+  }
+
+  public final class RippleAnimationKt {
+  }
+
+  public final class RippleKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
+    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+    field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+  }
+
+  public static final class RippleTheme.Companion {
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+  }
+
+  public final class RippleThemeKt {
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+  }
+
+}
+
diff --git a/compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf b/compose/material/material-ripple/api/res-current.txt
similarity index 100%
copy from compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf
copy to compose/material/material-ripple/api/res-current.txt
diff --git a/compose/material/material-ripple/api/restricted_current.txt b/compose/material/material-ripple/api/restricted_current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/restricted_current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+  @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+    method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+  }
+
+  public final class RippleAnimationKt {
+  }
+
+  public final class RippleKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
+    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+    field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+  }
+
+  public static final class RippleTheme.Companion {
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+  }
+
+  public final class RippleThemeKt {
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+  }
+
+}
+
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
new file mode 100644
index 0000000..296fd0d
--- /dev/null
+++ b/compose/material/material-ripple/build.gradle
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.AndroidXUiPlugin
+import androidx.build.LibraryGroups
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXUiPlugin")
+}
+
+AndroidXUiPlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+    kotlinPlugin project(":compose:compiler:compiler")
+
+    if(!AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block below
+         */
+        api project(":compose:foundation:foundation")
+        api project(":compose:runtime:runtime")
+
+        implementation(KOTLIN_STDLIB_COMMON)
+        implementation project(":compose:animation:animation")
+        implementation project(":compose:ui:ui-util")
+
+        testImplementation(ANDROIDX_TEST_RULES)
+        testImplementation(ANDROIDX_TEST_RUNNER)
+        testImplementation(JUNIT)
+        testImplementation(TRUTH)
+    }
+}
+
+if(AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+    kotlin {
+        android()
+        jvm("desktop")
+
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block above
+         */
+        sourceSets {
+            commonMain.dependencies {
+                implementation(KOTLIN_STDLIB_COMMON)
+                api project(":compose:foundation:foundation")
+                api project(":compose:runtime:runtime")
+
+                implementation project(":compose:animation:animation")
+                implementation project(":compose:ui:ui-util")
+            }
+
+            desktopMain.dependencies {
+                implementation(KOTLIN_STDLIB)
+            }
+
+            test.dependencies {
+                implementation(ANDROIDX_TEST_RULES)
+                implementation(ANDROIDX_TEST_RUNNER)
+                implementation(JUNIT)
+                implementation(TRUTH)
+            }
+        }
+    }
+}
+
+androidx {
+    name = "Compose Material Ripple"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.Compose.MATERIAL
+    inceptionYear = "2020"
+    description = "Material ripple used to build interactive components"
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        useIR = true
+    }
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-Xallow-jvm-ir-dependencies"]
+    }
+}
diff --git a/compose/material/material-ripple/src/androidMain/AndroidManifest.xml b/compose/material/material-ripple/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..0c2bb3c
--- /dev/null
+++ b/compose/material/material-ripple/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest package="androidx.compose.material.ripple" />
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
similarity index 75%
rename from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt
rename to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
index 0bfd44c..a4942eb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.gesture
+package androidx.compose.material.ripple
 
 @RequiresOptIn(
-    "This pointer input API is experimental and is likely to change before becoming " +
-        "stable."
+    "This ripple API is experimental and is likely to change or to be removed in" +
+        " the future."
 )
-annotation class ExperimentalPointerInput
\ No newline at end of file
+public annotation class ExperimentalRippleApi
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
similarity index 64%
rename from compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt
rename to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index f2a65ca..59d79ee 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -26,8 +26,6 @@
 import androidx.compose.foundation.IndicationInstance
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
@@ -35,7 +33,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.structuralEqualityPolicy
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
@@ -48,120 +46,134 @@
 import androidx.compose.ui.util.nativeClass
 
 /**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * Creates and [remember]s a Ripple using values provided by [RippleTheme].
  *
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
  *
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * A Ripple responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
+ * responds to other [Interaction]s by showing a fixed [StateLayer] with varying alpha values
+ * depending on the [Interaction].
+ *
+ * If you are using MaterialTheme in your hierarchy, a Ripple will be used as the default
+ * [Indication] inside components such as [androidx.compose.foundation.clickable] and
+ * [androidx.compose.foundation.indication]. You can also manually provide Ripples through
+ * [androidx.compose.foundation.AmbientIndication] for the same effect if you are not using
+ * MaterialTheme.
+ *
+ * You can also explicitly create a Ripple and provide it to components in order to change the
+ * parameters from the default, such as to create an unbounded ripple with a fixed size.
  *
  * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded
  * ripples always animate from the target layout center, bounded ripples animate from the touch
  * position.
- * @param radius Effects grow up to this size. If null is provided the size would be calculated
+ * @param radius the radius for the ripple. If `null` is provided then the size will be calculated
  * based on the target layout size.
- * @param color The Ripple color is usually the same color used by the text or iconography in the
- * component. If [Color.Unspecified] is provided the color will be calculated by
- * [RippleTheme.defaultColor]. This color will then have [RippleTheme.rippleOpacity] applied
+ * @param color the color of the ripple. This color is usually the same color used by the text or
+ * iconography in the component. This color will then have [RippleTheme.rippleAlpha] applied to
+ * calculate the final color used to draw the ripple. If [Color.Unspecified] is provided the color
+ * used will be [RippleTheme.defaultColor] instead.
  */
-@Suppress("ComposableNaming")
 @Deprecated(
-    "Replaced with rememberRippleIndication",
+    "Replaced with rememberRipple",
     ReplaceWith(
-        "rememberRippleIndication(bounded, radius, color)",
-        "androidx.compose.material.ripple.rememberRippleIndication"
+        "rememberRipple(bounded, radius, color)",
+        "androidx.compose.material.ripple.rememberRipple"
     )
 )
 @Composable
-@OptIn(ExperimentalMaterialApi::class)
-fun RippleIndication(
+@OptIn(ExperimentalRippleApi::class)
+public fun rememberRippleIndication(
     bounded: Boolean = true,
     radius: Dp? = null,
     color: Color = Color.Unspecified
-): RippleIndication {
+): Indication {
     val theme = AmbientRippleTheme.current
     val clock = AmbientAnimationClock.current.asDisposableClock()
     val resolvedColor = color.useOrElse { theme.defaultColor() }
     val colorState = remember { mutableStateOf(resolvedColor, structuralEqualityPolicy()) }
     colorState.value = resolvedColor
-    val interactionOpacity = theme.rippleOpacity()
+    val rippleAlpha = theme.rippleAlpha()
     return remember(bounded, radius, theme, clock) {
-        RippleIndication(bounded, radius, colorState, interactionOpacity, clock)
+        Ripple(bounded, radius ?: Dp.Unspecified, colorState, rippleAlpha, clock)
     }
 }
 
 /**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * Creates and [remember]s a Ripple using values provided by [RippleTheme].
  *
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
  *
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * A Ripple responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
+ * responds to other [Interaction]s by showing a fixed [StateLayer] with varying alpha values
+ * depending on the [Interaction].
+ *
+ * If you are using MaterialTheme in your hierarchy, a Ripple will be used as the default
+ * [Indication] inside components such as [androidx.compose.foundation.clickable] and
+ * [androidx.compose.foundation.indication]. You can also manually provide Ripples through
+ * [androidx.compose.foundation.AmbientIndication] for the same effect if you are not using
+ * MaterialTheme.
+ *
+ * You can also explicitly create a Ripple and provide it to components in order to change the
+ * parameters from the default, such as to create an unbounded ripple with a fixed size.
  *
  * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded
  * ripples always animate from the target layout center, bounded ripples animate from the touch
  * position.
- * @param radius Effects grow up to this size. If null is provided the size would be calculated
- * based on the target layout size.
- * @param color The Ripple color is usually the same color used by the text or iconography in the
- * component. If [Color.Unspecified] is provided the color will be calculated by
- * [RippleTheme.defaultColor]. This color will then have [RippleTheme.rippleOpacity] applied
+ * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be
+ * calculated based on the target layout size.
+ * @param color the color of the ripple. This color is usually the same color used by the text or
+ * iconography in the component. This color will then have [RippleTheme.rippleAlpha] applied to
+ * calculate the final color used to draw the ripple. If [Color.Unspecified] is provided the color
+ * used will be [RippleTheme.defaultColor] instead.
  */
 @Composable
-@OptIn(ExperimentalMaterialApi::class)
-fun rememberRippleIndication(
+@OptIn(ExperimentalRippleApi::class)
+public fun rememberRipple(
     bounded: Boolean = true,
-    radius: Dp? = null,
+    radius: Dp = Dp.Unspecified,
     color: Color = Color.Unspecified
-): RippleIndication {
+): Indication {
     val theme = AmbientRippleTheme.current
     val clock = AmbientAnimationClock.current.asDisposableClock()
     val resolvedColor = color.useOrElse { theme.defaultColor() }
     val colorState = remember { mutableStateOf(resolvedColor, structuralEqualityPolicy()) }
     colorState.value = resolvedColor
-    val interactionOpacity = theme.rippleOpacity()
+    val rippleAlpha = theme.rippleAlpha()
     return remember(bounded, radius, theme, clock) {
-        RippleIndication(bounded, radius, colorState, interactionOpacity, clock)
+        Ripple(bounded, radius, colorState, rippleAlpha, clock)
     }
 }
 
 /**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
  *
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
+ * responds to other [Interaction]s by showing a fixed [StateLayer] with varying alpha values
+ * depending on the [Interaction].
  *
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * If you are using MaterialTheme in your hierarchy, a Ripple will be used as the default
+ * [Indication] inside components such as [androidx.compose.foundation.clickable] and
+ * [androidx.compose.foundation.indication]. You can also manually provide Ripples through
+ * [androidx.compose.foundation.AmbientIndication] for the same effect if you are not using
+ * MaterialTheme.
+ *
+ * You can also explicitly create a Ripple and provide it to components in order to change the
+ * parameters from the default, such as to create an unbounded ripple with a fixed size.
  */
 @Stable
-@OptIn(ExperimentalMaterialApi::class)
-class RippleIndication internal constructor(
+@ExperimentalRippleApi
+private class Ripple(
     private val bounded: Boolean,
-    private val radius: Dp? = null,
-    private var color: State<Color>,
-    private val rippleOpacity: RippleOpacity,
+    private val radius: Dp,
+    private val color: State<Color>,
+    private val rippleAlpha: RippleAlpha,
     private val clock: AnimationClockObservable
 ) : Indication {
     override fun createInstance(): IndicationInstance {
-        return RippleIndicationInstance(bounded, radius, color, rippleOpacity, clock)
+        return RippleIndicationInstance(bounded, radius, color, rippleAlpha, clock)
     }
 
     // to force stability on this indication we need equals and hashcode, there's no value in
@@ -170,12 +182,12 @@
         if (this === other) return true
         if (this.nativeClass() != other?.nativeClass()) return false
 
-        other as RippleIndication
+        other as Ripple
 
         if (bounded != other.bounded) return false
         if (radius != other.radius) return false
         if (color != other.color) return false
-        if (rippleOpacity != other.rippleOpacity) return false
+        if (rippleAlpha != other.rippleAlpha) return false
         if (clock != other.clock) return false
 
         return true
@@ -183,24 +195,24 @@
 
     override fun hashCode(): Int {
         var result = bounded.hashCode()
-        result = 31 * result + (radius?.hashCode() ?: 0)
+        result = 31 * result + radius.hashCode()
         result = 31 * result + color.hashCode()
-        result = 31 * result + rippleOpacity.hashCode()
+        result = 31 * result + rippleAlpha.hashCode()
         result = 31 * result + clock.hashCode()
         return result
     }
 }
 
-@OptIn(ExperimentalMaterialApi::class)
+@ExperimentalRippleApi
 private class RippleIndicationInstance constructor(
     private val bounded: Boolean,
-    private val radius: Dp? = null,
-    private var color: State<Color>,
-    private val rippleOpacity: RippleOpacity,
+    private val radius: Dp,
+    private val color: State<Color>,
+    private val rippleAlpha: RippleAlpha,
     private val clock: AnimationClockObservable
 ) : IndicationInstance {
 
-    private val stateLayer = StateLayer(clock, bounded, rippleOpacity)
+    private val stateLayer = StateLayer(clock, bounded, rippleAlpha)
 
     private val ripples = mutableStateListOf<RippleAnimation>()
     private var currentPressPosition: Offset? = null
@@ -208,8 +220,12 @@
 
     override fun ContentDrawScope.drawIndication(interactionState: InteractionState) {
         val color = color.value
-        val targetRadius =
-            radius?.toPx() ?: getRippleEndRadius(bounded, size)
+        // TODO: b/174310811 use library function instead of manual logic here
+        val targetRadius = if (radius == Dp.Unspecified) {
+            getRippleEndRadius(bounded, size)
+        } else {
+            radius.toPx()
+        }
         drawContent()
         with(stateLayer) {
             drawStateLayer(interactionState, targetRadius, color)
@@ -250,7 +266,7 @@
     private fun DrawScope.drawRipples(color: Color) {
         ripples.fastForEach {
             with(it) {
-                val alpha = rippleOpacity.opacityForInteraction(Interaction.Pressed)
+                val alpha = rippleAlpha.alphaForInteraction(Interaction.Pressed)
                 if (alpha != 0f) {
                     draw(color.copy(alpha = alpha))
                 }
@@ -287,13 +303,13 @@
  * @see IncomingStateLayerAnimationSpecs
  * @see OutgoingStateLayerAnimationSpecs
  */
-@OptIn(ExperimentalMaterialApi::class)
+@ExperimentalRippleApi
 private class StateLayer(
     clock: AnimationClockObservable,
     private val bounded: Boolean,
-    private val rippleOpacity: RippleOpacity
+    private val rippleAlpha: RippleAlpha
 ) {
-    private val animatedOpacity = AnimatedFloatModel(0f, clock)
+    private val animatedAlpha = AnimatedFloatModel(0f, clock)
     private var previousInteractions: Set<Interaction> = emptySet()
     private var lastDrawnInteraction: Interaction? = null
 
@@ -318,15 +334,15 @@
             if (interaction in previousInteractions) continue
 
             // Move to the next interaction if this is not an interaction we show a state layer for
-            val targetOpacity = rippleOpacity.opacityForInteraction(interaction)
-            if (targetOpacity == 0f) continue
+            val targetAlpha = rippleAlpha.alphaForInteraction(interaction)
+            if (targetAlpha == 0f) continue
 
             // TODO: consider defaults - these will be used for a custom Interaction that we are
             // not aware of, but has an alpha that should be shown because of a custom RippleTheme.
             val incomingAnimationSpec = IncomingStateLayerAnimationSpecs[interaction]
                 ?: TweenSpec(durationMillis = 15, easing = LinearEasing)
 
-            animatedOpacity.animateTo(targetOpacity, incomingAnimationSpec)
+            animatedAlpha.animateTo(targetAlpha, incomingAnimationSpec)
 
             lastDrawnInteraction = interaction
             handled = true
@@ -342,7 +358,7 @@
                 val outgoingAnimationSpec = OutgoingStateLayerAnimationSpecs[previousInteraction]
                     ?: TweenSpec(durationMillis = 15, easing = LinearEasing)
 
-                animatedOpacity.animateTo(0f, outgoingAnimationSpec)
+                animatedAlpha.animateTo(0f, outgoingAnimationSpec)
 
                 lastDrawnInteraction = null
             }
@@ -350,10 +366,10 @@
 
         previousInteractions = currentInteractions
 
-        val opacity = animatedOpacity.value
+        val alpha = animatedAlpha.value
 
-        if (opacity > 0f) {
-            val modulatedColor = color.copy(alpha = opacity)
+        if (alpha > 0f) {
+            val modulatedColor = color.copy(alpha = alpha)
 
             if (bounded) {
                 clipRect {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
similarity index 93%
rename from compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
rename to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
index d1f06c2..18c5b76 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
@@ -41,19 +41,18 @@
 import kotlin.math.max
 
 /**
- * [RippleAnimation]s are drawn as part of [RippleIndication] as a visual indicator for an
- * [androidx.compose.foundation.Interaction.Pressed] state.
+ * [RippleAnimation]s are drawn as part of [Ripple] as a visual indicator for an
+ * different [androidx.compose.foundation.Interaction]s.
  *
  * Use [androidx.compose.foundation.clickable] or [androidx.compose.foundation.indication] to add a
- * [RippleIndication] to your component, which contains a RippleAnimation for pressed states, and
+ * ripple to your component, which contains a RippleAnimation for pressed states, and
  * a state layer for other states.
  *
  * This is a default implementation based on the Material Design specification.
  *
- * A circular ripple effect whose origin starts at the input touch point and
- * whose radius expands from 60% of the final value. The ripple origin
- * animates to the center of its target layout for the bounded version
- * and stays in the center for the unbounded one.
+ * Draws a circular ripple effect with an origin starting at the input touch point and with a
+ * radius expanding from 60% of the final value. The ripple origin animates to the center of its
+ * target layout for the bounded version and stays in the center for the unbounded one.
  *
  * @param size The size of the target layout.
  * @param startPosition The position the animation will start from.
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
new file mode 100644
index 0000000..d637ef9
--- /dev/null
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.ripple
+
+import androidx.compose.foundation.Interaction
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ProvidableAmbient
+import androidx.compose.runtime.staticAmbientOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.luminance
+
+/**
+ * Defines the appearance for Ripples. You can define a new theme and apply it using
+ * [AmbientRippleTheme]. See [defaultRippleColor] and [defaultRippleAlpha] for default values
+ * that can be used when creating your own [RippleTheme].
+ *
+ * @see rememberRipple
+ */
+@ExperimentalRippleApi
+public interface RippleTheme {
+    /**
+     * @return the default ripple color at the call site's position in the hierarchy.
+     * This color will be used when a color is not explicitly set in the ripple itself.
+     * @see defaultRippleColor
+     */
+    @Composable
+    public fun defaultColor(): Color
+
+    /**
+     * @return the [RippleAlpha] used to calculate the alpha for the ripple depending on the
+     * [Interaction] for a given component. This will be set as the alpha channel for
+     * [defaultColor] or the color explicitly provided to the ripple.
+     * @see defaultRippleAlpha
+     */
+    @Composable
+    public fun rippleAlpha(): RippleAlpha
+
+    public companion object {
+        /**
+         * Represents the default color that will be used for a ripple if a color has not been
+         * explicitly set on the ripple instance.
+         *
+         * @param contentColor the color of content (text or iconography) in the component that
+         * contains the ripple.
+         * @param lightTheme whether the theme is light or not
+         */
+        @ExperimentalRippleApi
+        public fun defaultRippleColor(
+            contentColor: Color,
+            lightTheme: Boolean
+        ): Color {
+            val contentLuminance = contentColor.luminance()
+            // If we are on a colored surface (typically indicated by low luminance content), the
+            // ripple color should be white.
+            return if (!lightTheme && contentLuminance < 0.5) {
+                Color.White
+                // Otherwise use contentColor
+            } else {
+                contentColor
+            }
+        }
+
+        /**
+         * Represents the default [RippleAlpha] that will be used for a ripple to indicate different
+         * states.
+         *
+         * @param contentColor the color of content (text or iconography) in the component that
+         * contains the ripple.
+         * @param lightTheme whether the theme is light or not
+         */
+        @ExperimentalRippleApi
+        public fun defaultRippleAlpha(contentColor: Color, lightTheme: Boolean): RippleAlpha {
+            return when {
+                lightTheme -> {
+                    if (contentColor.luminance() > 0.5) {
+                        LightThemeHighContrastRippleAlpha
+                    } else {
+                        LightThemeLowContrastRippleAlpha
+                    }
+                }
+                else -> {
+                    DarkThemeRippleAlpha
+                }
+            }
+        }
+    }
+}
+
+/**
+ * RippleAlpha defines the alpha of the ripple / state layer for a given [Interaction].
+ */
+@ExperimentalRippleApi
+public fun interface RippleAlpha {
+    /**
+     * @return the alpha of the ripple for the given [interaction]. Return `0f` if this
+     * particular interaction should not show a corresponding ripple / state layer.
+     */
+    public fun alphaForInteraction(interaction: Interaction): Float
+}
+
+/**
+ * Ambient used for providing [RippleTheme] down the tree.
+ *
+ * See [RippleTheme.defaultRippleColor] and [RippleTheme.defaultRippleAlpha] functions for the
+ * default implementations for color and alpha.
+ */
+@ExperimentalRippleApi
+public val AmbientRippleTheme: ProvidableAmbient<RippleTheme> = staticAmbientOf { DebugRippleTheme }
+
+@Suppress("unused")
+@OptIn(ExperimentalRippleApi::class)
+private sealed class DefaultRippleAlpha(
+    val pressed: Float,
+    val focused: Float,
+    val dragged: Float,
+    val hovered: Float
+) : RippleAlpha {
+    override fun alphaForInteraction(interaction: Interaction): Float = when (interaction) {
+        Interaction.Pressed -> pressed
+        Interaction.Dragged -> dragged
+        else -> 0f
+    }
+}
+
+/**
+ * Alpha values for high luminance content in a light theme.
+ *
+ * This content will typically be placed on colored surfaces, so it is important that the
+ * contrast here is higher to meet accessibility standards, and increase legibility.
+ *
+ * These levels are typically used for text / iconography in primary colored tabs /
+ * bottom navigation / etc.
+ */
+private object LightThemeHighContrastRippleAlpha : DefaultRippleAlpha(
+    pressed = 0.24f,
+    focused = 0.24f,
+    dragged = 0.16f,
+    hovered = 0.08f
+)
+
+/**
+ * Alpha levels for low luminance content in a light theme.
+ *
+ * This content will typically be placed on grayscale surfaces, so the contrast here can be lower
+ * without sacrificing accessibility and legibility.
+ *
+ * These levels are typically used for body text on the main surface (white in light theme, grey
+ * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc.
+ */
+private object LightThemeLowContrastRippleAlpha : DefaultRippleAlpha(
+    pressed = 0.12f,
+    focused = 0.12f,
+    dragged = 0.08f,
+    hovered = 0.04f
+)
+
+/**
+ * Alpha levels for all content in a dark theme.
+ */
+private object DarkThemeRippleAlpha : DefaultRippleAlpha(
+    pressed = 0.10f,
+    focused = 0.12f,
+    dragged = 0.08f,
+    hovered = 0.04f
+)
+
+/**
+ * Simple debug indication that will assume black content color and light theme. You should
+ * instead provide your own theme with meaningful values - this exists as an alternative to
+ * crashing if no theme is provided.
+ */
+@ExperimentalRippleApi
+@Immutable
+private object DebugRippleTheme : RippleTheme {
+    @Composable
+    override fun defaultColor() = RippleTheme.defaultRippleColor(Color.Black, lightTheme = true)
+
+    @Composable
+    override fun rippleAlpha(): RippleAlpha = RippleTheme.defaultRippleAlpha(
+        Color.Black,
+        lightTheme = true
+    )
+}
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt b/compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
rename to compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 2596cb5..866aca3 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -12,18 +12,32 @@
     method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  public final class BackdropScaffoldConstants {
-    method public float getDefaultFrontLayerElevation-D9Ej5fM();
-    method public long getDefaultFrontLayerScrimColor-0d7_KjU();
-    method public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
-    method public float getDefaultHeaderHeight-D9Ej5fM();
-    method public float getDefaultPeekHeight-D9Ej5fM();
+  @Deprecated public final class BackdropScaffoldConstants {
+    method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
+    method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
     property public final float DefaultFrontLayerElevation;
-    property public final long DefaultFrontLayerScrimColor;
-    property public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
+    property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
     property public final float DefaultHeaderHeight;
     property public final float DefaultPeekHeight;
-    field public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+  }
+
+  public final class BackdropScaffoldDefaults {
+    method public float getFrontLayerElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFrontLayerShape();
+    method public float getHeaderHeight-D9Ej5fM();
+    method public float getPeekHeight-D9Ej5fM();
+    property public final float FrontLayerElevation;
+    property public final float HeaderHeight;
+    property public final float PeekHeight;
+    property @androidx.compose.runtime.Composable public final long frontLayerScrimColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape frontLayerShape;
+    field public static final androidx.compose.material.BackdropScaffoldDefaults INSTANCE;
   }
 
   public final class BackdropScaffoldKt {
@@ -82,12 +96,20 @@
     method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
   }
 
-  public final class BottomSheetScaffoldConstants {
-    method public float getDefaultSheetElevation-D9Ej5fM();
-    method public float getDefaultSheetPeekHeight-D9Ej5fM();
+  @Deprecated public final class BottomSheetScaffoldConstants {
+    method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
+    method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
     property public final float DefaultSheetElevation;
     property public final float DefaultSheetPeekHeight;
-    field public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+  }
+
+  public final class BottomSheetScaffoldDefaults {
+    method public float getSheetElevation-D9Ej5fM();
+    method public float getSheetPeekHeight-D9Ej5fM();
+    property public final float SheetElevation;
+    property public final float SheetPeekHeight;
+    field public static final androidx.compose.material.BottomSheetScaffoldDefaults INSTANCE;
   }
 
   public final class BottomSheetScaffoldKt {
@@ -131,19 +153,19 @@
     method public long contentColor-0d7_KjU(boolean enabled);
   }
 
-  public final class ButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
-    method public float getDefaultIconSize-D9Ej5fM();
-    method public float getDefaultIconSpacing-D9Ej5fM();
-    method public float getDefaultMinHeight-D9Ej5fM();
-    method public float getDefaultMinWidth-D9Ej5fM();
-    method public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
-    method public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
-    method public float getOutlinedBorderSize-D9Ej5fM();
+  @Deprecated public final class ButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
+    method @Deprecated public float getDefaultIconSize-D9Ej5fM();
+    method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
+    method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
+    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
+    method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
     property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
     property public final float DefaultIconSize;
     property public final float DefaultIconSpacing;
@@ -151,8 +173,33 @@
     property public final float DefaultMinWidth;
     property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
     property public final float OutlinedBorderSize;
-    property public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
-    field public static final androidx.compose.material.ButtonConstants INSTANCE;
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
+    field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
+    field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
+  }
+
+  public final class ButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getIconSize-D9Ej5fM();
+    method public float getIconSpacing-D9Ej5fM();
+    method public float getMinHeight-D9Ej5fM();
+    method public float getMinWidth-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedBorder();
+    method public float getOutlinedBorderSize-D9Ej5fM();
+    method public androidx.compose.foundation.layout.PaddingValues getTextButtonContentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors outlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors textButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float IconSize;
+    property public final float IconSpacing;
+    property public final float MinHeight;
+    property public final float MinWidth;
+    property public final float OutlinedBorderSize;
+    property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedBorder;
+    field public static final androidx.compose.material.ButtonDefaults INSTANCE;
     field public static final float OutlinedBorderOpacity = 0.12f;
   }
 
@@ -176,9 +223,14 @@
     method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
   }
 
-  public final class CheckboxConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
-    field public static final androidx.compose.material.CheckboxConstants INSTANCE;
+  @Deprecated public final class CheckboxConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+    field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
+  }
+
+  public final class CheckboxDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+    field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
   }
 
   public final class CheckboxKt {
@@ -224,12 +276,12 @@
   }
 
   public final class ContentAlpha {
-    method public float getDisabled();
-    method public float getHigh();
-    method public float getMedium();
-    property public final float disabled;
-    property public final float high;
-    property public final float medium;
+    method @androidx.compose.runtime.Composable public float getDisabled();
+    method @androidx.compose.runtime.Composable public float getHigh();
+    method @androidx.compose.runtime.Composable public float getMedium();
+    property @androidx.compose.runtime.Composable public final float disabled;
+    property @androidx.compose.runtime.Composable public final float high;
+    property @androidx.compose.runtime.Composable public final float medium;
     field public static final androidx.compose.material.ContentAlpha INSTANCE;
   }
 
@@ -270,13 +322,22 @@
     method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
   }
 
-  public final class DrawerConstants {
-    method public float getDefaultElevation-D9Ej5fM();
-    method public long getDefaultScrimColor-0d7_KjU();
+  @Deprecated public final class DrawerConstants {
+    method @Deprecated public float getDefaultElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
     property public final float DefaultElevation;
-    property public final long defaultScrimColor;
-    field public static final androidx.compose.material.DrawerConstants INSTANCE;
-    field public static final float ScrimDefaultOpacity = 0.32f;
+    property @androidx.compose.runtime.Composable public final long defaultScrimColor;
+    field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
+    field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
+  }
+
+  public final class DrawerDefaults {
+    method public float getElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long scrimColor;
+    field public static final androidx.compose.material.DrawerDefaults INSTANCE;
+    field public static final float ScrimOpacity = 0.32f;
   }
 
   public final class DrawerKt {
@@ -306,10 +367,16 @@
     enum_constant public static final androidx.compose.material.DrawerValue Open;
   }
 
-  public final class ElevationConstants {
+  @Deprecated public final class ElevationConstants {
+    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+    field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
+  }
+
+  public final class ElevationDefaults {
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    field public static final androidx.compose.material.ElevationConstants INSTANCE;
+    field public static final androidx.compose.material.ElevationDefaults INSTANCE;
   }
 
   public final class ElevationKt {
@@ -335,12 +402,12 @@
   }
 
   @Deprecated public interface EmphasisLevels {
-    method @Deprecated public androidx.compose.material.Emphasis getDisabled();
-    method @Deprecated public androidx.compose.material.Emphasis getHigh();
-    method @Deprecated public androidx.compose.material.Emphasis getMedium();
-    property public abstract androidx.compose.material.Emphasis disabled;
-    property public abstract androidx.compose.material.Emphasis high;
-    property public abstract androidx.compose.material.Emphasis medium;
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
   }
 
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
@@ -356,9 +423,14 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
   }
 
-  public final class FloatingActionButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
-    field public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+  @Deprecated public final class FloatingActionButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+    field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+  }
+
+  public final class FloatingActionButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+    field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
   }
 
   @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
@@ -395,12 +467,12 @@
   }
 
   public final class MaterialTheme {
-    method public androidx.compose.material.Colors getColors();
-    method public androidx.compose.material.Shapes getShapes();
-    method public androidx.compose.material.Typography getTypography();
-    property public final androidx.compose.material.Colors colors;
-    property public final androidx.compose.material.Shapes shapes;
-    property public final androidx.compose.material.Typography typography;
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Colors getColors();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Shapes getShapes();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Typography getTypography();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Colors colors;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Shapes shapes;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Typography typography;
     field public static final androidx.compose.material.MaterialTheme INSTANCE;
   }
 
@@ -413,12 +485,20 @@
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  public final class ModalBottomSheetConstants {
-    method public float getDefaultElevation-D9Ej5fM();
-    method public long getDefaultScrimColor-0d7_KjU();
+  @Deprecated public final class ModalBottomSheetConstants {
+    method @Deprecated public float getDefaultElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
     property public final float DefaultElevation;
-    property public final long DefaultScrimColor;
-    field public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+    property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
+    field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+  }
+
+  public final class ModalBottomSheetDefaults {
+    method public float getElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long scrimColor;
+    field public static final androidx.compose.material.ModalBottomSheetDefaults INSTANCE;
   }
 
   public final class ModalBottomSheetKt {
@@ -450,13 +530,22 @@
     method @androidx.compose.runtime.Composable public static void OutlinedTextField-IIju55g(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
   }
 
-  public final class ProgressIndicatorConstants {
-    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
-    method public float getDefaultStrokeWidth-D9Ej5fM();
+  @Deprecated public final class ProgressIndicatorConstants {
+    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
+    method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
     property public final float DefaultStrokeWidth;
-    field public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
-    field public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+    field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
+    field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+  }
+
+  public final class ProgressIndicatorDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
+    method public float getStrokeWidth-D9Ej5fM();
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+    property public final float StrokeWidth;
+    field public static final androidx.compose.material.ProgressIndicatorDefaults INSTANCE;
+    field public static final float IndicatorBackgroundOpacity = 0.24f;
   }
 
   public final class ProgressIndicatorKt {
@@ -470,9 +559,14 @@
     method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
   }
 
-  public final class RadioButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
-    field public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+  @Deprecated public final class RadioButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+    field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+  }
+
+  public final class RadioButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+    field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
   }
 
   public final class RadioButtonKt {
@@ -525,8 +619,14 @@
   public final class ShapesKt {
   }
 
-  public final class SliderConstants {
-    field public static final androidx.compose.material.SliderConstants INSTANCE;
+  @Deprecated public final class SliderConstants {
+    field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
+    field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
+    field @Deprecated public static final float TickColorAlpha = 0.54f;
+  }
+
+  public final class SliderDefaults {
+    field public static final androidx.compose.material.SliderDefaults INSTANCE;
     field public static final float InactiveTrackColorAlpha = 0.24f;
     field public static final float TickColorAlpha = 0.54f;
   }
@@ -535,12 +635,12 @@
     method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
   }
 
-  public final class SnackbarConstants {
-    method public long getDefaultActionPrimaryColor-0d7_KjU();
-    method public long getDefaultBackgroundColor-0d7_KjU();
-    property public final long defaultActionPrimaryColor;
-    property public final long defaultBackgroundColor;
-    field public static final androidx.compose.material.SnackbarConstants INSTANCE;
+  @Deprecated public final class SnackbarConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
+    property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
+    property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
+    field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
   }
 
   @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
@@ -554,6 +654,14 @@
     property public abstract String message;
   }
 
+  public final class SnackbarDefaults {
+    method @androidx.compose.runtime.Composable public long getBackgroundColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public long getPrimaryActionColor-0d7_KjU();
+    property @androidx.compose.runtime.Composable public final long backgroundColor;
+    property @androidx.compose.runtime.Composable public final long primaryActionColor;
+    field public static final androidx.compose.material.SnackbarDefaults INSTANCE;
+  }
+
   public enum SnackbarDuration {
     enum_constant public static final androidx.compose.material.SnackbarDuration Indefinite;
     enum_constant public static final androidx.compose.material.SnackbarDuration Long;
@@ -605,13 +713,24 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
   }
 
-  public final class SwipeableConstants {
-    method public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
-    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
-    method public float getDefaultVelocityThreshold-D9Ej5fM();
+  @Deprecated public final class SwipeableConstants {
+    method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
+    method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
     property public final float DefaultVelocityThreshold;
-    field public static final androidx.compose.material.SwipeableConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
+    field @Deprecated public static final float StandardResistanceFactor = 10.0f;
+    field @Deprecated public static final float StiffResistanceFactor = 20.0f;
+  }
+
+  public final class SwipeableDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+    method public float getVelocityThreshold-D9Ej5fM();
+    method public androidx.compose.material.ResistanceConfig? resistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+    property public final float VelocityThreshold;
+    field public static final androidx.compose.material.SwipeableDefaults INSTANCE;
     field public static final float StandardResistanceFactor = 10.0f;
     field public static final float StiffResistanceFactor = 20.0f;
   }
@@ -631,6 +750,8 @@
     method public final T! getTargetValue();
     method public final T! getValue();
     method public final boolean isAnimationRunning();
+    method public final float performDrag(float delta);
+    method public final void performFling(float velocity, kotlin.jvm.functions.Function0<kotlin.Unit> onEnd);
     method @androidx.compose.material.ExperimentalMaterialApi public final void snapTo(T? targetValue);
     property public final float direction;
     property public final boolean isAnimationRunning;
@@ -651,27 +772,46 @@
     method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
   }
 
-  public final class SwitchConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
-    field public static final androidx.compose.material.SwitchConstants INSTANCE;
+  @Deprecated public final class SwitchConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+    field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
+  }
+
+  public final class SwitchDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+    field public static final androidx.compose.material.SwitchDefaults INSTANCE;
   }
 
   public final class SwitchKt {
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
   }
 
-  public final class TabConstants {
-    method @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
-    method @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
-    method public float getDefaultDividerThickness-D9Ej5fM();
-    method public float getDefaultIndicatorHeight-D9Ej5fM();
-    method public float getDefaultScrollableTabRowPadding-D9Ej5fM();
+  @Deprecated public final class TabConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+    method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+    method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+    method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
+    method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
     property public final float DefaultDividerThickness;
     property public final float DefaultIndicatorHeight;
     property public final float DefaultScrollableTabRowPadding;
-    field public static final float DefaultDividerOpacity = 0.12f;
-    field public static final androidx.compose.material.TabConstants INSTANCE;
+    field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
+    field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+    method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+    method public float getDividerThickness-D9Ej5fM();
+    method public float getIndicatorHeight-D9Ej5fM();
+    method public float getScrollableTabRowPadding-D9Ej5fM();
+    method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+    property public final float DividerThickness;
+    property public final float IndicatorHeight;
+    property public final float ScrollableTabRowPadding;
+    field public static final float DividerOpacity = 0.12f;
+    field public static final androidx.compose.material.TabDefaults INSTANCE;
   }
 
   public final class TabKt {
@@ -765,32 +905,3 @@
 
 }
 
-package androidx.compose.material.ripple {
-
-  public final class RippleAnimationKt {
-  }
-
-  @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
-    method public androidx.compose.foundation.IndicationInstance createInstance();
-  }
-
-  public final class RippleIndicationKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
-    method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
-    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
-  }
-
-  public final class RippleThemeKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
-  }
-
-}
-
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 2596cb5..866aca3 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -12,18 +12,32 @@
     method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  public final class BackdropScaffoldConstants {
-    method public float getDefaultFrontLayerElevation-D9Ej5fM();
-    method public long getDefaultFrontLayerScrimColor-0d7_KjU();
-    method public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
-    method public float getDefaultHeaderHeight-D9Ej5fM();
-    method public float getDefaultPeekHeight-D9Ej5fM();
+  @Deprecated public final class BackdropScaffoldConstants {
+    method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
+    method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
     property public final float DefaultFrontLayerElevation;
-    property public final long DefaultFrontLayerScrimColor;
-    property public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
+    property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
     property public final float DefaultHeaderHeight;
     property public final float DefaultPeekHeight;
-    field public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+  }
+
+  public final class BackdropScaffoldDefaults {
+    method public float getFrontLayerElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFrontLayerShape();
+    method public float getHeaderHeight-D9Ej5fM();
+    method public float getPeekHeight-D9Ej5fM();
+    property public final float FrontLayerElevation;
+    property public final float HeaderHeight;
+    property public final float PeekHeight;
+    property @androidx.compose.runtime.Composable public final long frontLayerScrimColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape frontLayerShape;
+    field public static final androidx.compose.material.BackdropScaffoldDefaults INSTANCE;
   }
 
   public final class BackdropScaffoldKt {
@@ -82,12 +96,20 @@
     method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
   }
 
-  public final class BottomSheetScaffoldConstants {
-    method public float getDefaultSheetElevation-D9Ej5fM();
-    method public float getDefaultSheetPeekHeight-D9Ej5fM();
+  @Deprecated public final class BottomSheetScaffoldConstants {
+    method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
+    method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
     property public final float DefaultSheetElevation;
     property public final float DefaultSheetPeekHeight;
-    field public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+  }
+
+  public final class BottomSheetScaffoldDefaults {
+    method public float getSheetElevation-D9Ej5fM();
+    method public float getSheetPeekHeight-D9Ej5fM();
+    property public final float SheetElevation;
+    property public final float SheetPeekHeight;
+    field public static final androidx.compose.material.BottomSheetScaffoldDefaults INSTANCE;
   }
 
   public final class BottomSheetScaffoldKt {
@@ -131,19 +153,19 @@
     method public long contentColor-0d7_KjU(boolean enabled);
   }
 
-  public final class ButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
-    method public float getDefaultIconSize-D9Ej5fM();
-    method public float getDefaultIconSpacing-D9Ej5fM();
-    method public float getDefaultMinHeight-D9Ej5fM();
-    method public float getDefaultMinWidth-D9Ej5fM();
-    method public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
-    method public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
-    method public float getOutlinedBorderSize-D9Ej5fM();
+  @Deprecated public final class ButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
+    method @Deprecated public float getDefaultIconSize-D9Ej5fM();
+    method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
+    method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
+    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
+    method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
     property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
     property public final float DefaultIconSize;
     property public final float DefaultIconSpacing;
@@ -151,8 +173,33 @@
     property public final float DefaultMinWidth;
     property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
     property public final float OutlinedBorderSize;
-    property public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
-    field public static final androidx.compose.material.ButtonConstants INSTANCE;
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
+    field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
+    field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
+  }
+
+  public final class ButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getIconSize-D9Ej5fM();
+    method public float getIconSpacing-D9Ej5fM();
+    method public float getMinHeight-D9Ej5fM();
+    method public float getMinWidth-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedBorder();
+    method public float getOutlinedBorderSize-D9Ej5fM();
+    method public androidx.compose.foundation.layout.PaddingValues getTextButtonContentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors outlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors textButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float IconSize;
+    property public final float IconSpacing;
+    property public final float MinHeight;
+    property public final float MinWidth;
+    property public final float OutlinedBorderSize;
+    property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedBorder;
+    field public static final androidx.compose.material.ButtonDefaults INSTANCE;
     field public static final float OutlinedBorderOpacity = 0.12f;
   }
 
@@ -176,9 +223,14 @@
     method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
   }
 
-  public final class CheckboxConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
-    field public static final androidx.compose.material.CheckboxConstants INSTANCE;
+  @Deprecated public final class CheckboxConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+    field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
+  }
+
+  public final class CheckboxDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+    field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
   }
 
   public final class CheckboxKt {
@@ -224,12 +276,12 @@
   }
 
   public final class ContentAlpha {
-    method public float getDisabled();
-    method public float getHigh();
-    method public float getMedium();
-    property public final float disabled;
-    property public final float high;
-    property public final float medium;
+    method @androidx.compose.runtime.Composable public float getDisabled();
+    method @androidx.compose.runtime.Composable public float getHigh();
+    method @androidx.compose.runtime.Composable public float getMedium();
+    property @androidx.compose.runtime.Composable public final float disabled;
+    property @androidx.compose.runtime.Composable public final float high;
+    property @androidx.compose.runtime.Composable public final float medium;
     field public static final androidx.compose.material.ContentAlpha INSTANCE;
   }
 
@@ -270,13 +322,22 @@
     method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
   }
 
-  public final class DrawerConstants {
-    method public float getDefaultElevation-D9Ej5fM();
-    method public long getDefaultScrimColor-0d7_KjU();
+  @Deprecated public final class DrawerConstants {
+    method @Deprecated public float getDefaultElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
     property public final float DefaultElevation;
-    property public final long defaultScrimColor;
-    field public static final androidx.compose.material.DrawerConstants INSTANCE;
-    field public static final float ScrimDefaultOpacity = 0.32f;
+    property @androidx.compose.runtime.Composable public final long defaultScrimColor;
+    field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
+    field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
+  }
+
+  public final class DrawerDefaults {
+    method public float getElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long scrimColor;
+    field public static final androidx.compose.material.DrawerDefaults INSTANCE;
+    field public static final float ScrimOpacity = 0.32f;
   }
 
   public final class DrawerKt {
@@ -306,10 +367,16 @@
     enum_constant public static final androidx.compose.material.DrawerValue Open;
   }
 
-  public final class ElevationConstants {
+  @Deprecated public final class ElevationConstants {
+    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+    field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
+  }
+
+  public final class ElevationDefaults {
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    field public static final androidx.compose.material.ElevationConstants INSTANCE;
+    field public static final androidx.compose.material.ElevationDefaults INSTANCE;
   }
 
   public final class ElevationKt {
@@ -335,12 +402,12 @@
   }
 
   @Deprecated public interface EmphasisLevels {
-    method @Deprecated public androidx.compose.material.Emphasis getDisabled();
-    method @Deprecated public androidx.compose.material.Emphasis getHigh();
-    method @Deprecated public androidx.compose.material.Emphasis getMedium();
-    property public abstract androidx.compose.material.Emphasis disabled;
-    property public abstract androidx.compose.material.Emphasis high;
-    property public abstract androidx.compose.material.Emphasis medium;
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
   }
 
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
@@ -356,9 +423,14 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
   }
 
-  public final class FloatingActionButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
-    field public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+  @Deprecated public final class FloatingActionButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+    field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+  }
+
+  public final class FloatingActionButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+    field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
   }
 
   @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
@@ -395,12 +467,12 @@
   }
 
   public final class MaterialTheme {
-    method public androidx.compose.material.Colors getColors();
-    method public androidx.compose.material.Shapes getShapes();
-    method public androidx.compose.material.Typography getTypography();
-    property public final androidx.compose.material.Colors colors;
-    property public final androidx.compose.material.Shapes shapes;
-    property public final androidx.compose.material.Typography typography;
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Colors getColors();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Shapes getShapes();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Typography getTypography();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Colors colors;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Shapes shapes;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Typography typography;
     field public static final androidx.compose.material.MaterialTheme INSTANCE;
   }
 
@@ -413,12 +485,20 @@
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  public final class ModalBottomSheetConstants {
-    method public float getDefaultElevation-D9Ej5fM();
-    method public long getDefaultScrimColor-0d7_KjU();
+  @Deprecated public final class ModalBottomSheetConstants {
+    method @Deprecated public float getDefaultElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
     property public final float DefaultElevation;
-    property public final long DefaultScrimColor;
-    field public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+    property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
+    field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+  }
+
+  public final class ModalBottomSheetDefaults {
+    method public float getElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long scrimColor;
+    field public static final androidx.compose.material.ModalBottomSheetDefaults INSTANCE;
   }
 
   public final class ModalBottomSheetKt {
@@ -450,13 +530,22 @@
     method @androidx.compose.runtime.Composable public static void OutlinedTextField-IIju55g(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
   }
 
-  public final class ProgressIndicatorConstants {
-    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
-    method public float getDefaultStrokeWidth-D9Ej5fM();
+  @Deprecated public final class ProgressIndicatorConstants {
+    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
+    method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
     property public final float DefaultStrokeWidth;
-    field public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
-    field public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+    field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
+    field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+  }
+
+  public final class ProgressIndicatorDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
+    method public float getStrokeWidth-D9Ej5fM();
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+    property public final float StrokeWidth;
+    field public static final androidx.compose.material.ProgressIndicatorDefaults INSTANCE;
+    field public static final float IndicatorBackgroundOpacity = 0.24f;
   }
 
   public final class ProgressIndicatorKt {
@@ -470,9 +559,14 @@
     method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
   }
 
-  public final class RadioButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
-    field public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+  @Deprecated public final class RadioButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+    field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+  }
+
+  public final class RadioButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+    field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
   }
 
   public final class RadioButtonKt {
@@ -525,8 +619,14 @@
   public final class ShapesKt {
   }
 
-  public final class SliderConstants {
-    field public static final androidx.compose.material.SliderConstants INSTANCE;
+  @Deprecated public final class SliderConstants {
+    field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
+    field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
+    field @Deprecated public static final float TickColorAlpha = 0.54f;
+  }
+
+  public final class SliderDefaults {
+    field public static final androidx.compose.material.SliderDefaults INSTANCE;
     field public static final float InactiveTrackColorAlpha = 0.24f;
     field public static final float TickColorAlpha = 0.54f;
   }
@@ -535,12 +635,12 @@
     method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
   }
 
-  public final class SnackbarConstants {
-    method public long getDefaultActionPrimaryColor-0d7_KjU();
-    method public long getDefaultBackgroundColor-0d7_KjU();
-    property public final long defaultActionPrimaryColor;
-    property public final long defaultBackgroundColor;
-    field public static final androidx.compose.material.SnackbarConstants INSTANCE;
+  @Deprecated public final class SnackbarConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
+    property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
+    property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
+    field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
   }
 
   @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
@@ -554,6 +654,14 @@
     property public abstract String message;
   }
 
+  public final class SnackbarDefaults {
+    method @androidx.compose.runtime.Composable public long getBackgroundColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public long getPrimaryActionColor-0d7_KjU();
+    property @androidx.compose.runtime.Composable public final long backgroundColor;
+    property @androidx.compose.runtime.Composable public final long primaryActionColor;
+    field public static final androidx.compose.material.SnackbarDefaults INSTANCE;
+  }
+
   public enum SnackbarDuration {
     enum_constant public static final androidx.compose.material.SnackbarDuration Indefinite;
     enum_constant public static final androidx.compose.material.SnackbarDuration Long;
@@ -605,13 +713,24 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
   }
 
-  public final class SwipeableConstants {
-    method public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
-    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
-    method public float getDefaultVelocityThreshold-D9Ej5fM();
+  @Deprecated public final class SwipeableConstants {
+    method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
+    method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
     property public final float DefaultVelocityThreshold;
-    field public static final androidx.compose.material.SwipeableConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
+    field @Deprecated public static final float StandardResistanceFactor = 10.0f;
+    field @Deprecated public static final float StiffResistanceFactor = 20.0f;
+  }
+
+  public final class SwipeableDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+    method public float getVelocityThreshold-D9Ej5fM();
+    method public androidx.compose.material.ResistanceConfig? resistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+    property public final float VelocityThreshold;
+    field public static final androidx.compose.material.SwipeableDefaults INSTANCE;
     field public static final float StandardResistanceFactor = 10.0f;
     field public static final float StiffResistanceFactor = 20.0f;
   }
@@ -631,6 +750,8 @@
     method public final T! getTargetValue();
     method public final T! getValue();
     method public final boolean isAnimationRunning();
+    method public final float performDrag(float delta);
+    method public final void performFling(float velocity, kotlin.jvm.functions.Function0<kotlin.Unit> onEnd);
     method @androidx.compose.material.ExperimentalMaterialApi public final void snapTo(T? targetValue);
     property public final float direction;
     property public final boolean isAnimationRunning;
@@ -651,27 +772,46 @@
     method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
   }
 
-  public final class SwitchConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
-    field public static final androidx.compose.material.SwitchConstants INSTANCE;
+  @Deprecated public final class SwitchConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+    field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
+  }
+
+  public final class SwitchDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+    field public static final androidx.compose.material.SwitchDefaults INSTANCE;
   }
 
   public final class SwitchKt {
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
   }
 
-  public final class TabConstants {
-    method @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
-    method @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
-    method public float getDefaultDividerThickness-D9Ej5fM();
-    method public float getDefaultIndicatorHeight-D9Ej5fM();
-    method public float getDefaultScrollableTabRowPadding-D9Ej5fM();
+  @Deprecated public final class TabConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+    method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+    method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+    method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
+    method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
     property public final float DefaultDividerThickness;
     property public final float DefaultIndicatorHeight;
     property public final float DefaultScrollableTabRowPadding;
-    field public static final float DefaultDividerOpacity = 0.12f;
-    field public static final androidx.compose.material.TabConstants INSTANCE;
+    field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
+    field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+    method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+    method public float getDividerThickness-D9Ej5fM();
+    method public float getIndicatorHeight-D9Ej5fM();
+    method public float getScrollableTabRowPadding-D9Ej5fM();
+    method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+    property public final float DividerThickness;
+    property public final float IndicatorHeight;
+    property public final float ScrollableTabRowPadding;
+    field public static final float DividerOpacity = 0.12f;
+    field public static final androidx.compose.material.TabDefaults INSTANCE;
   }
 
   public final class TabKt {
@@ -765,32 +905,3 @@
 
 }
 
-package androidx.compose.material.ripple {
-
-  public final class RippleAnimationKt {
-  }
-
-  @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
-    method public androidx.compose.foundation.IndicationInstance createInstance();
-  }
-
-  public final class RippleIndicationKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
-    method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
-    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
-  }
-
-  public final class RippleThemeKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
-  }
-
-}
-
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 2596cb5..866aca3 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -12,18 +12,32 @@
     method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  public final class BackdropScaffoldConstants {
-    method public float getDefaultFrontLayerElevation-D9Ej5fM();
-    method public long getDefaultFrontLayerScrimColor-0d7_KjU();
-    method public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
-    method public float getDefaultHeaderHeight-D9Ej5fM();
-    method public float getDefaultPeekHeight-D9Ej5fM();
+  @Deprecated public final class BackdropScaffoldConstants {
+    method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
+    method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
     property public final float DefaultFrontLayerElevation;
-    property public final long DefaultFrontLayerScrimColor;
-    property public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
+    property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
     property public final float DefaultHeaderHeight;
     property public final float DefaultPeekHeight;
-    field public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+  }
+
+  public final class BackdropScaffoldDefaults {
+    method public float getFrontLayerElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFrontLayerShape();
+    method public float getHeaderHeight-D9Ej5fM();
+    method public float getPeekHeight-D9Ej5fM();
+    property public final float FrontLayerElevation;
+    property public final float HeaderHeight;
+    property public final float PeekHeight;
+    property @androidx.compose.runtime.Composable public final long frontLayerScrimColor;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape frontLayerShape;
+    field public static final androidx.compose.material.BackdropScaffoldDefaults INSTANCE;
   }
 
   public final class BackdropScaffoldKt {
@@ -82,12 +96,20 @@
     method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
   }
 
-  public final class BottomSheetScaffoldConstants {
-    method public float getDefaultSheetElevation-D9Ej5fM();
-    method public float getDefaultSheetPeekHeight-D9Ej5fM();
+  @Deprecated public final class BottomSheetScaffoldConstants {
+    method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
+    method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
     property public final float DefaultSheetElevation;
     property public final float DefaultSheetPeekHeight;
-    field public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+  }
+
+  public final class BottomSheetScaffoldDefaults {
+    method public float getSheetElevation-D9Ej5fM();
+    method public float getSheetPeekHeight-D9Ej5fM();
+    property public final float SheetElevation;
+    property public final float SheetPeekHeight;
+    field public static final androidx.compose.material.BottomSheetScaffoldDefaults INSTANCE;
   }
 
   public final class BottomSheetScaffoldKt {
@@ -131,19 +153,19 @@
     method public long contentColor-0d7_KjU(boolean enabled);
   }
 
-  public final class ButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
-    method public float getDefaultIconSize-D9Ej5fM();
-    method public float getDefaultIconSpacing-D9Ej5fM();
-    method public float getDefaultMinHeight-D9Ej5fM();
-    method public float getDefaultMinWidth-D9Ej5fM();
-    method public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
-    method public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
-    method public float getOutlinedBorderSize-D9Ej5fM();
+  @Deprecated public final class ButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
+    method @Deprecated public float getDefaultIconSize-D9Ej5fM();
+    method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
+    method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
+    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
+    method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
     property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
     property public final float DefaultIconSize;
     property public final float DefaultIconSpacing;
@@ -151,8 +173,33 @@
     property public final float DefaultMinWidth;
     property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
     property public final float OutlinedBorderSize;
-    property public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
-    field public static final androidx.compose.material.ButtonConstants INSTANCE;
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
+    field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
+    field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
+  }
+
+  public final class ButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+    method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getIconSize-D9Ej5fM();
+    method public float getIconSpacing-D9Ej5fM();
+    method public float getMinHeight-D9Ej5fM();
+    method public float getMinWidth-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedBorder();
+    method public float getOutlinedBorderSize-D9Ej5fM();
+    method public androidx.compose.foundation.layout.PaddingValues getTextButtonContentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors outlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors textButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+    property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float IconSize;
+    property public final float IconSpacing;
+    property public final float MinHeight;
+    property public final float MinWidth;
+    property public final float OutlinedBorderSize;
+    property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
+    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedBorder;
+    field public static final androidx.compose.material.ButtonDefaults INSTANCE;
     field public static final float OutlinedBorderOpacity = 0.12f;
   }
 
@@ -176,9 +223,14 @@
     method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
   }
 
-  public final class CheckboxConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
-    field public static final androidx.compose.material.CheckboxConstants INSTANCE;
+  @Deprecated public final class CheckboxConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+    field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
+  }
+
+  public final class CheckboxDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+    field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
   }
 
   public final class CheckboxKt {
@@ -224,12 +276,12 @@
   }
 
   public final class ContentAlpha {
-    method public float getDisabled();
-    method public float getHigh();
-    method public float getMedium();
-    property public final float disabled;
-    property public final float high;
-    property public final float medium;
+    method @androidx.compose.runtime.Composable public float getDisabled();
+    method @androidx.compose.runtime.Composable public float getHigh();
+    method @androidx.compose.runtime.Composable public float getMedium();
+    property @androidx.compose.runtime.Composable public final float disabled;
+    property @androidx.compose.runtime.Composable public final float high;
+    property @androidx.compose.runtime.Composable public final float medium;
     field public static final androidx.compose.material.ContentAlpha INSTANCE;
   }
 
@@ -270,13 +322,22 @@
     method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
   }
 
-  public final class DrawerConstants {
-    method public float getDefaultElevation-D9Ej5fM();
-    method public long getDefaultScrimColor-0d7_KjU();
+  @Deprecated public final class DrawerConstants {
+    method @Deprecated public float getDefaultElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
     property public final float DefaultElevation;
-    property public final long defaultScrimColor;
-    field public static final androidx.compose.material.DrawerConstants INSTANCE;
-    field public static final float ScrimDefaultOpacity = 0.32f;
+    property @androidx.compose.runtime.Composable public final long defaultScrimColor;
+    field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
+    field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
+  }
+
+  public final class DrawerDefaults {
+    method public float getElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long scrimColor;
+    field public static final androidx.compose.material.DrawerDefaults INSTANCE;
+    field public static final float ScrimOpacity = 0.32f;
   }
 
   public final class DrawerKt {
@@ -306,10 +367,16 @@
     enum_constant public static final androidx.compose.material.DrawerValue Open;
   }
 
-  public final class ElevationConstants {
+  @Deprecated public final class ElevationConstants {
+    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+    field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
+  }
+
+  public final class ElevationDefaults {
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    field public static final androidx.compose.material.ElevationConstants INSTANCE;
+    field public static final androidx.compose.material.ElevationDefaults INSTANCE;
   }
 
   public final class ElevationKt {
@@ -335,12 +402,12 @@
   }
 
   @Deprecated public interface EmphasisLevels {
-    method @Deprecated public androidx.compose.material.Emphasis getDisabled();
-    method @Deprecated public androidx.compose.material.Emphasis getHigh();
-    method @Deprecated public androidx.compose.material.Emphasis getMedium();
-    property public abstract androidx.compose.material.Emphasis disabled;
-    property public abstract androidx.compose.material.Emphasis high;
-    property public abstract androidx.compose.material.Emphasis medium;
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
+    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
   }
 
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
@@ -356,9 +423,14 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
   }
 
-  public final class FloatingActionButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
-    field public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+  @Deprecated public final class FloatingActionButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+    field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+  }
+
+  public final class FloatingActionButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+    field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
   }
 
   @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
@@ -395,12 +467,12 @@
   }
 
   public final class MaterialTheme {
-    method public androidx.compose.material.Colors getColors();
-    method public androidx.compose.material.Shapes getShapes();
-    method public androidx.compose.material.Typography getTypography();
-    property public final androidx.compose.material.Colors colors;
-    property public final androidx.compose.material.Shapes shapes;
-    property public final androidx.compose.material.Typography typography;
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Colors getColors();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Shapes getShapes();
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Typography getTypography();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Colors colors;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Shapes shapes;
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Typography typography;
     field public static final androidx.compose.material.MaterialTheme INSTANCE;
   }
 
@@ -413,12 +485,20 @@
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  public final class ModalBottomSheetConstants {
-    method public float getDefaultElevation-D9Ej5fM();
-    method public long getDefaultScrimColor-0d7_KjU();
+  @Deprecated public final class ModalBottomSheetConstants {
+    method @Deprecated public float getDefaultElevation-D9Ej5fM();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
     property public final float DefaultElevation;
-    property public final long DefaultScrimColor;
-    field public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+    property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
+    field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+  }
+
+  public final class ModalBottomSheetDefaults {
+    method public float getElevation-D9Ej5fM();
+    method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+    property public final float Elevation;
+    property @androidx.compose.runtime.Composable public final long scrimColor;
+    field public static final androidx.compose.material.ModalBottomSheetDefaults INSTANCE;
   }
 
   public final class ModalBottomSheetKt {
@@ -450,13 +530,22 @@
     method @androidx.compose.runtime.Composable public static void OutlinedTextField-IIju55g(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
   }
 
-  public final class ProgressIndicatorConstants {
-    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
-    method public float getDefaultStrokeWidth-D9Ej5fM();
+  @Deprecated public final class ProgressIndicatorConstants {
+    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
+    method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
     property public final float DefaultStrokeWidth;
-    field public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
-    field public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+    field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
+    field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+  }
+
+  public final class ProgressIndicatorDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
+    method public float getStrokeWidth-D9Ej5fM();
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+    property public final float StrokeWidth;
+    field public static final androidx.compose.material.ProgressIndicatorDefaults INSTANCE;
+    field public static final float IndicatorBackgroundOpacity = 0.24f;
   }
 
   public final class ProgressIndicatorKt {
@@ -470,9 +559,14 @@
     method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
   }
 
-  public final class RadioButtonConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
-    field public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+  @Deprecated public final class RadioButtonConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+    field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+  }
+
+  public final class RadioButtonDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+    field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
   }
 
   public final class RadioButtonKt {
@@ -525,8 +619,14 @@
   public final class ShapesKt {
   }
 
-  public final class SliderConstants {
-    field public static final androidx.compose.material.SliderConstants INSTANCE;
+  @Deprecated public final class SliderConstants {
+    field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
+    field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
+    field @Deprecated public static final float TickColorAlpha = 0.54f;
+  }
+
+  public final class SliderDefaults {
+    field public static final androidx.compose.material.SliderDefaults INSTANCE;
     field public static final float InactiveTrackColorAlpha = 0.24f;
     field public static final float TickColorAlpha = 0.54f;
   }
@@ -535,12 +635,12 @@
     method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
   }
 
-  public final class SnackbarConstants {
-    method public long getDefaultActionPrimaryColor-0d7_KjU();
-    method public long getDefaultBackgroundColor-0d7_KjU();
-    property public final long defaultActionPrimaryColor;
-    property public final long defaultBackgroundColor;
-    field public static final androidx.compose.material.SnackbarConstants INSTANCE;
+  @Deprecated public final class SnackbarConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
+    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
+    property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
+    property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
+    field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
   }
 
   @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
@@ -554,6 +654,14 @@
     property public abstract String message;
   }
 
+  public final class SnackbarDefaults {
+    method @androidx.compose.runtime.Composable public long getBackgroundColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public long getPrimaryActionColor-0d7_KjU();
+    property @androidx.compose.runtime.Composable public final long backgroundColor;
+    property @androidx.compose.runtime.Composable public final long primaryActionColor;
+    field public static final androidx.compose.material.SnackbarDefaults INSTANCE;
+  }
+
   public enum SnackbarDuration {
     enum_constant public static final androidx.compose.material.SnackbarDuration Indefinite;
     enum_constant public static final androidx.compose.material.SnackbarDuration Long;
@@ -605,13 +713,24 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
   }
 
-  public final class SwipeableConstants {
-    method public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
-    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
-    method public float getDefaultVelocityThreshold-D9Ej5fM();
+  @Deprecated public final class SwipeableConstants {
+    method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
+    method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
     property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
     property public final float DefaultVelocityThreshold;
-    field public static final androidx.compose.material.SwipeableConstants INSTANCE;
+    field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
+    field @Deprecated public static final float StandardResistanceFactor = 10.0f;
+    field @Deprecated public static final float StiffResistanceFactor = 20.0f;
+  }
+
+  public final class SwipeableDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+    method public float getVelocityThreshold-D9Ej5fM();
+    method public androidx.compose.material.ResistanceConfig? resistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+    property public final float VelocityThreshold;
+    field public static final androidx.compose.material.SwipeableDefaults INSTANCE;
     field public static final float StandardResistanceFactor = 10.0f;
     field public static final float StiffResistanceFactor = 20.0f;
   }
@@ -631,6 +750,8 @@
     method public final T! getTargetValue();
     method public final T! getValue();
     method public final boolean isAnimationRunning();
+    method public final float performDrag(float delta);
+    method public final void performFling(float velocity, kotlin.jvm.functions.Function0<kotlin.Unit> onEnd);
     method @androidx.compose.material.ExperimentalMaterialApi public final void snapTo(T? targetValue);
     property public final float direction;
     property public final boolean isAnimationRunning;
@@ -651,27 +772,46 @@
     method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
   }
 
-  public final class SwitchConstants {
-    method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
-    field public static final androidx.compose.material.SwitchConstants INSTANCE;
+  @Deprecated public final class SwitchConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+    field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
+  }
+
+  public final class SwitchDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+    field public static final androidx.compose.material.SwitchDefaults INSTANCE;
   }
 
   public final class SwitchKt {
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
   }
 
-  public final class TabConstants {
-    method @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
-    method @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
-    method public float getDefaultDividerThickness-D9Ej5fM();
-    method public float getDefaultIndicatorHeight-D9Ej5fM();
-    method public float getDefaultScrollableTabRowPadding-D9Ej5fM();
+  @Deprecated public final class TabConstants {
+    method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+    method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+    method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+    method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
+    method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
+    method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
     property public final float DefaultDividerThickness;
     property public final float DefaultIndicatorHeight;
     property public final float DefaultScrollableTabRowPadding;
-    field public static final float DefaultDividerOpacity = 0.12f;
-    field public static final androidx.compose.material.TabConstants INSTANCE;
+    field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
+    field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+    method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+    method public float getDividerThickness-D9Ej5fM();
+    method public float getIndicatorHeight-D9Ej5fM();
+    method public float getScrollableTabRowPadding-D9Ej5fM();
+    method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+    property public final float DividerThickness;
+    property public final float IndicatorHeight;
+    property public final float ScrollableTabRowPadding;
+    field public static final float DividerOpacity = 0.12f;
+    field public static final androidx.compose.material.TabDefaults INSTANCE;
   }
 
   public final class TabKt {
@@ -765,32 +905,3 @@
 
 }
 
-package androidx.compose.material.ripple {
-
-  public final class RippleAnimationKt {
-  }
-
-  @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
-    method public androidx.compose.foundation.IndicationInstance createInstance();
-  }
-
-  public final class RippleIndicationKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
-    method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
-    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
-  }
-
-  public final class RippleThemeKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
-  }
-
-}
-
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 2053ed0..96b2108 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -17,7 +17,6 @@
 
 import androidx.build.AndroidXUiPlugin
 import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
 import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -42,6 +41,7 @@
         api project(":compose:animation:animation-core")
         api project(":compose:foundation:foundation")
         api project(":compose:material:material-icons-core")
+        api project(":compose:material:material-ripple")
         api project(":compose:runtime:runtime")
         api project(":compose:ui:ui")
         api project(":compose:ui:ui-text")
@@ -86,6 +86,7 @@
                 api project(":compose:animation:animation-core")
                 api project(":compose:foundation:foundation")
                 api project(":compose:material:material-icons-core")
+                api project(":compose:material:material-ripple")
                 api project(":compose:runtime:runtime")
                 api project(":compose:ui:ui")
                 api project(":compose:ui:ui-text")
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
index db79704..cf2c613 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
@@ -28,7 +28,7 @@
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.shape.GenericShape
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.OutlinedButton
 import androidx.compose.material.Text
@@ -78,7 +78,7 @@
     Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
         Button(
             onClick = {},
-            colors = ButtonConstants.defaultButtonColors(
+            colors = ButtonDefaults.buttonColors(
                 backgroundColor = MaterialTheme.colors.secondary
             )
         ) {
@@ -135,7 +135,7 @@
         onClick = {},
         modifier = Modifier.preferredSize(110.dp),
         shape = TriangleShape,
-        colors = ButtonConstants.defaultOutlinedButtonColors(
+        colors = ButtonDefaults.outlinedButtonColors(
             backgroundColor = Color.Yellow
         ),
         border = BorderStroke(width = 2.dp, color = Color.Black)
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt
index cd681e6..ae05869 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ColorPickerDemo.kt
@@ -61,7 +61,7 @@
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.graphics.SweepGradient
+import androidx.compose.ui.graphics.SweepGradientShader
 import androidx.compose.ui.graphics.isSpecified
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.toPixelMap
@@ -309,9 +309,8 @@
 private class ColorWheel(diameter: Int) {
     private val radius = diameter / 2f
 
-    // TODO: b/152063545 - replace with Compose SweepGradient when it is available
-    private val sweepGradient = SweepGradient(
-        listOf(
+    private val sweepGradient = SweepGradientShader(
+        colors = listOf(
             Color.Red,
             Color.Magenta,
             Color.Blue,
@@ -320,13 +319,14 @@
             Color.Yellow,
             Color.Red
         ),
-        Offset(radius, radius)
+        colorStops = null,
+        center = Offset(radius, radius)
     )
 
     val image = ImageBitmap(diameter, diameter).also { imageBitmap ->
         val canvas = Canvas(imageBitmap)
         val center = Offset(radius, radius)
-        val paint = Paint().apply { sweepGradient.applyTo(this, 1.0f) }
+        val paint = Paint().apply { shader = sweepGradient }
         canvas.drawCircle(center, radius, paint)
     }
 }
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
index 3b8a880..834ffbc5 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
@@ -41,7 +41,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -88,9 +87,7 @@
 private fun DynamicThemeApp(scrollFraction: ScrollFraction, palette: Colors) {
     MaterialTheme(palette) {
         val scrollState = rememberScrollState()
-        val fraction =
-            round((scrollState.value / scrollState.maxValue) * 100) / 100
-        remember(fraction) { scrollFraction.value = fraction }
+        scrollFraction.value = round((scrollState.value / scrollState.maxValue) * 100) / 100
         Scaffold(
             topBar = { TopAppBar({ Text("Scroll down!") }) },
             bottomBar = { BottomAppBar(cutoutShape = CircleShape) {} },
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ListItemDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ListItemDemo.kt
index 9b53c84..f0af72f 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ListItemDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ListItemDemo.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ScrollableColumn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Call
+import androidx.compose.material.samples.ClickableListItems
 import androidx.compose.material.samples.OneLineListItems
 import androidx.compose.material.samples.OneLineRtlLtrListItems
 import androidx.compose.material.samples.ThreeLineListItems
@@ -35,6 +36,7 @@
     val icon56 = imageResource(R.drawable.ic_android)
     val vectorIcon = Icons.Default.Call
     ScrollableColumn {
+        ClickableListItems()
         OneLineListItems(icon24, icon40, icon56, vectorIcon)
         TwoLineListItems(icon24, icon40)
         ThreeLineListItems(icon24, vectorIcon)
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
index ddbfc47..0dcc59c 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
@@ -85,7 +85,7 @@
         expanded = expanded,
         onDismissRequest = { expanded = false },
         toggle = iconButton,
-        dropdownOffset = Position(-12.dp, -12.dp),
+        dropdownOffset = Position(24.dp, 0.dp),
         toggleModifier = modifier
     ) {
         options.forEach {
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
index 1d503d0..cb37e1c 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
@@ -21,7 +21,7 @@
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Text
 import androidx.compose.material.samples.FancyIndicatorContainerTabs
 import androidx.compose.material.samples.FancyIndicatorTabs
@@ -69,7 +69,7 @@
             onClick = {
                 showingSimple.value = !showingSimple.value
             },
-            colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.Cyan)
+            colors = ButtonDefaults.buttonColors(backgroundColor = Color.Cyan)
         ) {
             Text(buttonText)
         }
diff --git a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
index 35c4da7..077d3a1 100644
--- a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
+++ b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
@@ -32,7 +32,7 @@
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Surface
 import androidx.compose.material.Text
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
@@ -76,7 +76,7 @@
                 .selectable(
                     selected = selected,
                     onClick = onSelected,
-                    indication = rememberRippleIndication(bounded = false)
+                    indication = rememberRipple(bounded = false)
                 )
         ) {
             Icon(icon, tint = tabTintColor)
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
index ecbfef5..0294cad 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
@@ -18,9 +18,7 @@
 
 import androidx.annotation.Sampled
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.BackdropScaffold
 import androidx.compose.material.BackdropValue
 import androidx.compose.material.ExperimentalMaterialApi
@@ -40,7 +38,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
@@ -88,22 +85,27 @@
             )
         },
         backLayerContent = {
-            LazyColumnFor((1..5).toList()) {
-                ListItem(
-                    Modifier.clickable {
-                        selection.value = it
-                        scaffoldState.conceal()
-                    },
-                    text = { Text("Select $it") }
-                )
+            LazyColumn {
+                for (i in 1..5) item {
+                    ListItem(
+                        Modifier.clickable {
+                            selection.value = i
+                            scaffoldState.conceal()
+                        },
+                        text = { Text("Select $i") }
+                    )
+                }
             }
         },
         frontLayerContent = {
-            Box(
-                Modifier.fillMaxSize(),
-                contentAlignment = Alignment.Center
-            ) {
-                Text("Selection: ${selection.value}")
+            Text("Selection: ${selection.value}")
+            LazyColumn {
+                for (i in 1..50) item {
+                    ListItem(
+                        text = { Text("Item $i") },
+                        icon = { Icon(Icons.Default.Favorite) }
+                    )
+                }
             }
         }
     )
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
index e2ac96e..12cfe41 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
@@ -20,7 +20,7 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Icon
 import androidx.compose.material.OutlinedButton
 import androidx.compose.material.Text
@@ -58,8 +58,8 @@
 @Composable
 fun ButtonWithIconSample() {
     Button(onClick = { /* Do something! */ }) {
-        Icon(Icons.Filled.Favorite, Modifier.size(ButtonConstants.DefaultIconSize))
-        Spacer(Modifier.size(ButtonConstants.DefaultIconSpacing))
+        Icon(Icons.Filled.Favorite, Modifier.size(ButtonDefaults.IconSize))
+        Spacer(Modifier.size(ButtonDefaults.IconSpacing))
         Text("Like")
     }
 }
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt
index efb6f3c..88f67fd 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ListSamples.kt
@@ -19,12 +19,16 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.selection.toggleable
 import androidx.compose.material.AmbientContentColor
 import androidx.compose.material.Checkbox
 import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.Icon
 import androidx.compose.material.ListItem
+import androidx.compose.material.Switch
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -35,6 +39,57 @@
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.semantics.clearAndSetSemantics
+
+@Sampled
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun ClickableListItems() {
+    Column {
+        var switched by remember { mutableStateOf(false) }
+        val onSwitchedChange: (Boolean) -> Unit = { switched = it }
+        ListItem(
+            text = { Text("Switch ListItem") },
+            trailing = {
+                // The [clearAndSetSemantics] causes the switch's redundant
+                // toggleable semantics to be cleared in favor of the [ListItem]
+                // toggleable's, to improve usability with screen-readers.
+                Box(Modifier.clearAndSetSemantics {}) {
+                    Switch(
+                        checked = switched,
+                        onCheckedChange = onSwitchedChange
+                    )
+                }
+            },
+            modifier = Modifier.toggleable(
+                value = switched,
+                onValueChange = onSwitchedChange
+            )
+        )
+        Divider()
+        var checked by remember { mutableStateOf(true) }
+        val onCheckedChange: (Boolean) -> Unit = { checked = it }
+        ListItem(
+            text = { Text("Checkbox ListItem") },
+            trailing = {
+                // The [clearAndSetSemantics] causes the checkbox's redundant
+                // toggleable semantics to be cleared in favor of the [ListItem]
+                // toggleable's, to improve usability with screen-readers.
+                Box(Modifier.clearAndSetSemantics {}) {
+                    Checkbox(
+                        checked = checked,
+                        onCheckedChange = onCheckedChange
+                    )
+                }
+            },
+            modifier = Modifier.toggleable(
+                value = checked,
+                onValueChange = onCheckedChange
+            )
+        )
+        Divider()
+    }
+}
 
 @Sampled
 @Composable
@@ -131,21 +186,6 @@
             }
         )
         Divider()
-        var checked by remember { mutableStateOf(false) }
-        ListItem(
-            text = { Text("Two line list item") },
-            secondaryText = { Text("Secondary text") },
-            icon = {
-                Image(
-                    icon40x40,
-                    colorFilter = ColorFilter.tint(AmbientContentColor.current)
-                )
-            },
-            trailing = {
-                Checkbox(checked, onCheckedChange = { checked = !checked })
-            }
-        )
-        Divider()
     }
 }
 
@@ -266,22 +306,6 @@
         )
         Divider()
         ListItem(
-            text = { Text("Clickable two line item") },
-            secondaryText = { Text("Secondary text") },
-            icon = {
-                Image(
-                    icon40x40,
-                    colorFilter = ColorFilter.tint(AmbientContentColor.current)
-                )
-            },
-            trailing = {
-                var checked by remember { mutableStateOf(false) }
-                Checkbox(checked, onCheckedChange = { checked = !checked })
-            },
-            modifier = Modifier.clickable { }
-        )
-        Divider()
-        ListItem(
             text = { Text("بندان قابلان للنقر") },
             secondaryText = { Text("نص ثانوي") },
             icon = {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
index 6f72f97..7261692 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Button
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.Icon
@@ -45,11 +46,13 @@
     ModalBottomSheetLayout(
         sheetState = state,
         sheetContent = {
-            for (i in 1..5) {
-                ListItem(
-                    text = { Text("Item $i") },
-                    icon = { Icon(Icons.Default.Favorite) }
-                )
+            LazyColumn {
+                for (i in 1..50) item {
+                    ListItem(
+                        text = { Text("Item $i") },
+                        icon = { Icon(Icons.Default.Favorite) }
+                    )
+                }
             }
         }
     ) {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt
index 8c81625..ab3277e 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt
@@ -24,7 +24,7 @@
 import androidx.compose.material.CircularProgressIndicator
 import androidx.compose.material.LinearProgressIndicator
 import androidx.compose.material.OutlinedButton
-import androidx.compose.material.ProgressIndicatorConstants
+import androidx.compose.material.ProgressIndicatorDefaults
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -41,7 +41,7 @@
     var progress by remember { mutableStateOf(0.1f) }
     val animatedProgress = animate(
         target = progress,
-        animSpec = ProgressIndicatorConstants.DefaultProgressAnimationSpec
+        animSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
     )
 
     Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -63,7 +63,7 @@
     var progress by remember { mutableStateOf(0.1f) }
     val animatedProgress = animate(
         target = progress,
-        animSpec = ProgressIndicatorConstants.DefaultProgressAnimationSpec
+        animSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
     )
 
     Column(horizontalAlignment = Alignment.CenterHorizontally) {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt
index b4664b66..67c5f45 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -24,7 +25,7 @@
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.selection.selectable
 import androidx.compose.material.Checkbox
-import androidx.compose.material.CheckboxConstants
+import androidx.compose.material.CheckboxDefaults
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.RadioButton
 import androidx.compose.material.Switch
@@ -37,6 +38,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.unit.dp
 
@@ -64,7 +66,7 @@
         TriStateCheckbox(
             state = parentState,
             onClick = onParentClick,
-            colors = CheckboxConstants.defaultColors(
+            colors = CheckboxDefaults.colors(
                 checkedColor = MaterialTheme.colors.primary
             )
         )
@@ -130,10 +132,15 @@
                     .padding(horizontal = 16.dp),
                 verticalAlignment = Alignment.CenterVertically
             ) {
-                RadioButton(
-                    selected = (text == selectedOption),
-                    onClick = { onOptionSelected(text) }
-                )
+                // The [clearAndSetSemantics] causes the button's redundant
+                // selectable semantics to be cleared in favor of the [Row]
+                // selectable's, to improve usability with screen-readers.
+                Box(Modifier.clearAndSetSemantics {}) {
+                    RadioButton(
+                        selected = (text == selectedOption),
+                        onClick = { onOptionSelected(text) }
+                    )
+                }
                 Text(
                     text = text,
                     style = MaterialTheme.typography.body1.merge(),
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
index cfa1849..ca1b39d 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
@@ -22,7 +22,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Card
 import androidx.compose.material.DismissDirection.EndToStart
 import androidx.compose.material.DismissDirection.StartToEnd
@@ -78,57 +78,63 @@
     // will animate to red if you're swiping left or green if you're swiping right. When you let
     // go, the item will animate out of the way if you're swiping left (like deleting an email) or
     // back to its default position if you're swiping right (like marking an email as read/unread).
-    LazyColumnFor(items) { item ->
-        var unread by remember { mutableStateOf(false) }
-        val dismissState = rememberDismissState(
-            confirmStateChange = {
-                if (it == DismissedToEnd) unread = !unread
-                it != DismissedToEnd
-            }
-        )
-        SwipeToDismiss(
-            state = dismissState,
-            modifier = Modifier.padding(vertical = 4.dp),
-            directions = setOf(StartToEnd, EndToStart),
-            dismissThresholds = { direction ->
-                FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
-            },
-            background = {
-                val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
-                val color = animate(
-                    when (dismissState.targetValue) {
-                        Default -> Color.LightGray
-                        DismissedToEnd -> Color.Green
-                        DismissedToStart -> Color.Red
-                    }
-                )
-                val alignment = when (direction) {
-                    StartToEnd -> Alignment.CenterStart
-                    EndToStart -> Alignment.CenterEnd
+    LazyColumn {
+        items(items) { item ->
+            var unread by remember { mutableStateOf(false) }
+            val dismissState = rememberDismissState(
+                confirmStateChange = {
+                    if (it == DismissedToEnd) unread = !unread
+                    it != DismissedToEnd
                 }
-                val icon = when (direction) {
-                    StartToEnd -> Icons.Default.Done
-                    EndToStart -> Icons.Default.Delete
-                }
-                val scale = animate(if (dismissState.targetValue == Default) 0.75f else 1f)
-
-                Box(
-                    modifier = Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
-                    contentAlignment = alignment
-                ) {
-                    Icon(icon, Modifier.scale(scale))
-                }
-            },
-            dismissContent = {
-                Card(
-                    elevation = animate(if (dismissState.dismissDirection != null) 4.dp else 0.dp)
-                ) {
-                    ListItem(
-                        text = { Text(item, fontWeight = if (unread) FontWeight.Bold else null) },
-                        secondaryText = { Text("Swipe me left or right!") }
+            )
+            SwipeToDismiss(
+                state = dismissState,
+                modifier = Modifier.padding(vertical = 4.dp),
+                directions = setOf(StartToEnd, EndToStart),
+                dismissThresholds = { direction ->
+                    FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
+                },
+                background = {
+                    val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
+                    val color = animate(
+                        when (dismissState.targetValue) {
+                            Default -> Color.LightGray
+                            DismissedToEnd -> Color.Green
+                            DismissedToStart -> Color.Red
+                        }
                     )
+                    val alignment = when (direction) {
+                        StartToEnd -> Alignment.CenterStart
+                        EndToStart -> Alignment.CenterEnd
+                    }
+                    val icon = when (direction) {
+                        StartToEnd -> Icons.Default.Done
+                        EndToStart -> Icons.Default.Delete
+                    }
+                    val scale = animate(if (dismissState.targetValue == Default) 0.75f else 1f)
+
+                    Box(
+                        Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
+                        contentAlignment = alignment
+                    ) {
+                        Icon(icon, Modifier.scale(scale))
+                    }
+                },
+                dismissContent = {
+                    Card(
+                        elevation = animate(
+                            if (dismissState.dismissDirection != null) 4.dp else 0.dp
+                        )
+                    ) {
+                        ListItem(
+                            text = {
+                                Text(item, fontWeight = if (unread) FontWeight.Bold else null)
+                            },
+                            secondaryText = { Text("Swipe me left or right!") }
+                        )
+                    }
                 }
-            }
-        )
+            )
+        }
     }
 }
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt
index 933fd45..1ca2897 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt
@@ -33,8 +33,10 @@
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import kotlin.math.roundToInt
 
 @Sampled
 @Composable
@@ -62,7 +64,7 @@
     ) {
         Box(
             Modifier
-                .offset(x = { swipeableState.offset.value })
+                .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                 .preferredSize(squareSize)
                 .background(Color.Red),
             contentAlignment = Alignment.Center
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
index b76d5a8..a6b9745 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
@@ -41,7 +41,7 @@
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.ScrollableTabRow
 import androidx.compose.material.Tab
-import androidx.compose.material.TabConstants.defaultTabIndicatorOffset
+import androidx.compose.material.TabDefaults.tabIndicatorOffset
 import androidx.compose.material.TabPosition
 import androidx.compose.material.TabRow
 import androidx.compose.material.Text
@@ -189,7 +189,7 @@
 
     // Reuse the default offset animation modifier, but use our own indicator
     val indicator = @Composable { tabPositions: List<TabPosition> ->
-        FancyIndicator(Color.White, Modifier.defaultTabIndicatorOffset(tabPositions[state]))
+        FancyIndicator(Color.White, Modifier.tabIndicatorOffset(tabPositions[state]))
     }
 
     Column {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
index 4cc00ec..05b8283 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -58,7 +59,7 @@
                 TopAppBar(title = { Text("Title") })
             }
             .assertHeightIsEqualTo(appBarHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -169,7 +170,7 @@
                 BottomAppBar {}
             }
             .assertHeightIsEqualTo(appBarHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
index d18dde2..c219aec 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.test.assertIsSelected
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.isSelectable
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -63,7 +64,7 @@
         rule.setMaterialContentForSizeAssertions {
             BottomNavigationSample()
         }
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
             .assertHeightIsEqualTo(height)
     }
 
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
index 03c84ef..452942f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
@@ -395,7 +395,7 @@
                     Button(
                         onClick = {},
                         enabled = false,
-                        colors = ButtonConstants.defaultButtonColors(
+                        colors = ButtonDefaults.buttonColors(
                             backgroundColor = Color.Red
                         ),
                         shape = RectangleShape
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
index a938532..1d4aa77 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material
 
 import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -41,7 +42,7 @@
                 Divider()
             }
             .assertHeightIsEqualTo(defaultHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -51,7 +52,7 @@
             .setMaterialContentForSizeAssertions {
                 Divider(thickness = height)
             }
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
             .assertHeightIsEqualTo(height)
     }
 
@@ -65,6 +66,6 @@
                 Divider(startIndent = indent, thickness = height)
             }
             .assertHeightIsEqualTo(height)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 }
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
index f81b3d9..9f831e1 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
@@ -182,7 +182,7 @@
                     FloatingActionButton(
                         modifier = Modifier.testTag("myButton"),
                         onClick = {},
-                        elevation = FloatingActionButtonConstants.defaultElevation(
+                        elevation = FloatingActionButtonDefaults.elevation(
                             defaultElevation = 0.dp
                         )
                     ) {
@@ -219,7 +219,7 @@
                     ExtendedFloatingActionButton(
                         modifier = Modifier.testTag("myButton"),
                         onClick = {},
-                        elevation = FloatingActionButtonConstants.defaultElevation(
+                        elevation = FloatingActionButtonDefaults.elevation(
                             defaultElevation = 0.dp
                         ),
                         text = { Box(Modifier.preferredSize(10.dp, 50.dp)) }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
index fe7511d..809ca2f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.node.Ref
 import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
@@ -57,7 +58,7 @@
                 ListItem(text = { Text("Primary text") })
             }
             .assertHeightIsEqualTo(expectedHeightNoIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -71,7 +72,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightSmallIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -85,7 +86,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightLargeIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -99,7 +100,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightNoIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -115,7 +116,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightWithIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -130,7 +131,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -145,7 +146,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -161,7 +162,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -177,7 +178,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
similarity index 86%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt
rename to compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
index 6e5dc86..2bd7a2d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.ripple
+package androidx.compose.material
 
 import android.os.Build
+import androidx.compose.foundation.Indication
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.indication
@@ -26,12 +27,11 @@
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.GOLDEN_MATERIAL
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
+import androidx.compose.material.ripple.AmbientRippleTheme
+import androidx.compose.material.ripple.ExperimentalRippleApi
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.getValue
@@ -61,11 +61,15 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+/**
+ * Test for the [RippleTheme] provided by [MaterialTheme], to verify colors and opacity in
+ * different configurations.
+ */
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class)
-class RippleIndicationTest {
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class, ExperimentalRippleApi::class)
+class MaterialRippleThemeTest {
 
     @get:Rule
     val rule = createComposeRule()
@@ -89,7 +93,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_light_highluminance_pressed",
+            "ripple_bounded_light_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
         )
     }
@@ -110,7 +114,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_light_highluminance_dragged",
+            "ripple_bounded_light_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
         )
     }
@@ -131,7 +135,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_light_lowluminance_pressed",
+            "ripple_bounded_light_lowluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
         )
     }
@@ -152,7 +156,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_light_lowluminance_dragged",
+            "ripple_bounded_light_lowluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -173,7 +177,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_dark_highluminance_pressed",
+            "ripple_bounded_dark_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
         )
     }
@@ -194,7 +198,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_dark_highluminance_dragged",
+            "ripple_bounded_dark_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -215,7 +219,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_dark_lowluminance_pressed",
+            "ripple_bounded_dark_lowluminance_pressed",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
         )
@@ -237,7 +241,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_dark_lowluminance_dragged",
+            "ripple_bounded_dark_lowluminance_dragged",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
         )
@@ -259,7 +263,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_light_highluminance_pressed",
+            "ripple_unbounded_light_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
         )
     }
@@ -280,7 +284,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_light_highluminance_dragged",
+            "ripple_unbounded_light_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
         )
     }
@@ -301,7 +305,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_light_lowluminance_pressed",
+            "ripple_unbounded_light_lowluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
         )
     }
@@ -322,7 +326,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_light_lowluminance_dragged",
+            "ripple_unbounded_light_lowluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -343,7 +347,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_dark_highluminance_pressed",
+            "ripple_unbounded_dark_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
         )
     }
@@ -364,7 +368,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_dark_highluminance_dragged",
+            "ripple_unbounded_dark_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -385,7 +389,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_dark_lowluminance_pressed",
+            "ripple_unbounded_dark_lowluminance_pressed",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
         )
@@ -407,7 +411,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_dark_lowluminance_dragged",
+            "ripple_unbounded_dark_lowluminance_dragged",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
         )
@@ -427,17 +431,15 @@
             override fun defaultColor() = rippleColor
 
             @Composable
-            override fun rippleOpacity() = object : RippleOpacity {
-                override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
-            }
+            override fun rippleAlpha() = RippleAlpha { rippleAlpha }
         }
 
         rule.setContent {
-            Providers(AmbientRippleTheme provides rippleTheme) {
-                MaterialTheme {
+            MaterialTheme {
+                Providers(AmbientRippleTheme provides rippleTheme) {
                     Surface(contentColor = contentColor) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                            RippleBox(interactionState, rememberRippleIndication())
+                            RippleBox(interactionState, rememberRipple())
                         }
                     }
                 }
@@ -449,7 +451,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_customtheme_pressed",
+            "ripple_customtheme_pressed",
             expectedColor
         )
     }
@@ -468,17 +470,15 @@
             override fun defaultColor() = rippleColor
 
             @Composable
-            override fun rippleOpacity() = object : RippleOpacity {
-                override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
-            }
+            override fun rippleAlpha() = RippleAlpha { rippleAlpha }
         }
 
         rule.setContent {
-            Providers(AmbientRippleTheme provides rippleTheme) {
-                MaterialTheme {
+            MaterialTheme {
+                Providers(AmbientRippleTheme provides rippleTheme) {
                     Surface(contentColor = contentColor) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                            RippleBox(interactionState, rememberRippleIndication())
+                            RippleBox(interactionState, rememberRipple())
                         }
                     }
                 }
@@ -490,7 +490,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_customtheme_dragged",
+            "ripple_customtheme_dragged",
             expectedColor
         )
     }
@@ -504,9 +504,7 @@
             override fun defaultColor() = color
 
             @Composable
-            override fun rippleOpacity() = object : RippleOpacity {
-                override fun opacityForInteraction(interaction: Interaction) = alpha
-            }
+            override fun rippleAlpha() = RippleAlpha { alpha }
         }
 
         val initialColor = Color.Red
@@ -515,11 +513,11 @@
         var rippleTheme by mutableStateOf(createRippleTheme(initialColor, initialAlpha))
 
         rule.setContent {
-            Providers(AmbientRippleTheme provides rippleTheme) {
-                MaterialTheme {
+            MaterialTheme {
+                Providers(AmbientRippleTheme provides rippleTheme) {
                     Surface(contentColor = Color.Black) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                            RippleBox(interactionState, rememberRippleIndication())
+                            RippleBox(interactionState, rememberRipple())
                         }
                     }
                 }
@@ -616,14 +614,14 @@
 }
 
 /**
- * Generic Button like component that allows injecting a [RippleIndication] and also includes
+ * Generic Button like component that allows injecting an [Indication] and also includes
  * padding around the rippled surface, so screenshots will include some dead space for clarity.
  *
  * @param interactionState the [InteractionState] that is used to drive the ripple state
- * @param rippleIndication [RippleIndication] placed inside the surface
+ * @param ripple ripple [Indication] placed inside the surface
  */
 @Composable
-private fun RippleBox(interactionState: InteractionState, rippleIndication: RippleIndication) {
+private fun RippleBox(interactionState: InteractionState, ripple: Indication) {
     Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
         Surface(
             Modifier.padding(25.dp),
@@ -632,7 +630,7 @@
             Box(
                 Modifier.preferredWidth(80.dp).preferredHeight(50.dp).indication(
                     interactionState = interactionState,
-                    indication = rippleIndication
+                    indication = ripple
                 )
             )
         }
@@ -659,7 +657,7 @@
         MaterialTheme(colors) {
             Surface(contentColor = contentColor) {
                 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                    RippleBox(interactionState, rememberRippleIndication(bounded))
+                    RippleBox(interactionState, rememberRipple(bounded))
                 }
             }
         }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
index fcd18b4..d55c5dc 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
@@ -22,20 +22,20 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.layout.LastBaseline
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.platform.AndroidOwner
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.getAlignmentLinePosition
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
 
 fun ComposeTestRule.setMaterialContent(
     modifier: Modifier = Modifier,
@@ -61,39 +61,9 @@
 fun SemanticsNodeInteraction.assertIsSquareWithSize(expectedSize: Dp) =
     assertWidthIsEqualTo(expectedSize).assertHeightIsEqualTo(expectedSize)
 
-fun SemanticsNodeInteraction.assertWidthFillsRoot(): SemanticsNodeInteraction {
-    val node = fetchSemanticsNode("Failed to assertWidthFillsScreen")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-    val rootViewWidth = owner.view.width
+fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
 
-    with(owner.density) {
-        node.boundsInRoot.width.toDp().assertIsEqualTo(rootViewWidth.toDp())
-    }
-    return this
-}
-
-fun ComposeTestRule.rootWidth(): Dp {
-    val nodeInteraction = onRoot()
-    val node = nodeInteraction.fetchSemanticsNode("Failed to get screen width")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-
-    return with(owner.density) {
-        owner.view.width.toDp()
-    }
-}
-
-fun ComposeTestRule.rootHeight(): Dp {
-    val nodeInteraction = onRoot()
-    val node = nodeInteraction.fetchSemanticsNode("Failed to get screen height")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-
-    return with(owner.density) {
-        owner.view.height.toDp()
-    }
-}
+fun ComposeTestRule.rootHeight(): Dp = onRoot().getUnclippedBoundsInRoot().height
 
 /**
  * Constant to emulate very big but finite constraints
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
index f33a0a9..6cbca52 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
@@ -21,8 +21,10 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.selection.SelectionContainer
+import androidx.compose.ui.selection.TextSelectionColors
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.height
@@ -36,6 +38,7 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.common.truth.Truth
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -59,6 +62,29 @@
     val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL)
 
     @Test
+    fun rememberTextSelectionColors() {
+        var lightTextSelectionColors: TextSelectionColors? = null
+        var darkTextSelectionColors: TextSelectionColors? = null
+        var lightPrimary: Color = Color.Unspecified
+        var darkPrimary: Color = Color.Unspecified
+        rule.setContent {
+            val lightColors = lightColors()
+            val darkColors = darkColors()
+            lightPrimary = lightColors.primary
+            darkPrimary = darkColors.primary
+            lightTextSelectionColors = rememberTextSelectionColors(lightColors)
+            darkTextSelectionColors = rememberTextSelectionColors(darkColors)
+        }
+
+        Truth.assertThat(lightTextSelectionColors!!.handleColor).isEqualTo(lightPrimary)
+        Truth.assertThat(darkTextSelectionColors!!.handleColor).isEqualTo(darkPrimary)
+        Truth.assertThat(lightTextSelectionColors!!.backgroundColor)
+            .isEqualTo(lightPrimary.copy(alpha = 0.325f))
+        Truth.assertThat(darkTextSelectionColors!!.backgroundColor)
+            .isEqualTo(darkPrimary.copy(alpha = 0.375f))
+    }
+
+    @Test
     fun text_lightThemeSelectionColors() {
         rule.setContent {
             TextTestContent(lightColors())
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index 29fdbcd..aff45d5 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -144,7 +144,7 @@
         )
 
         assertThat(ltrPosition.x).isEqualTo(
-            anchorPosition.x + anchorSize.width + offsetX
+            anchorPosition.x + offsetX
         )
         assertThat(ltrPosition.y).isEqualTo(
             anchorPosition.y + anchorSize.height + offsetY
@@ -161,7 +161,7 @@
         )
 
         assertThat(rtlPosition.x).isEqualTo(
-            anchorPosition.x - popupSize.width - offsetX
+            anchorPosition.x + anchorSize.width - offsetX - popupSize.width
         )
         assertThat(rtlPosition.y).isEqualTo(
             anchorPosition.y + anchorSize.height + offsetY
@@ -192,7 +192,7 @@
         )
 
         assertThat(ltrPosition.x).isEqualTo(
-            anchorPosition.x - popupSize.width - offsetX
+            anchorPosition.x + anchorSize.width - offsetX - popupSize.width
         )
         assertThat(ltrPosition.y).isEqualTo(
             anchorPosition.y - popupSize.height - offsetY
@@ -209,7 +209,7 @@
         )
 
         assertThat(rtlPosition.x).isEqualTo(
-            anchorPositionRtl.x + anchorSize.width + offsetX
+            anchorPositionRtl.x + offsetX
         )
         assertThat(rtlPosition.y).isEqualTo(
             anchorPositionRtl.y - popupSize.height - offsetY
@@ -217,6 +217,35 @@
     }
 
     @Test
+    fun menu_positioning_top() {
+        val screenWidth = 500
+        val screenHeight = 1000
+        val density = Density(1f)
+        val windowBounds = IntBounds(0, 0, screenWidth, screenHeight)
+        val anchorPosition = IntOffset(0, 0)
+        val anchorSize = IntSize(50, 20)
+        val popupSize = IntSize(150, 500)
+
+        // The min margin above and below the menu, relative to the screen.
+        val MenuVerticalMargin = 32.dp
+        val verticalMargin = with(density) { MenuVerticalMargin.toIntPx() }
+
+        val position = DropdownMenuPositionProvider(
+            Position(0.dp, 0.dp),
+            density
+        ).calculatePosition(
+            IntBounds(anchorPosition, anchorSize),
+            windowBounds,
+            LayoutDirection.Ltr,
+            popupSize
+        )
+
+        assertThat(position.y).isEqualTo(
+            verticalMargin
+        )
+    }
+
+    @Test
     fun menu_positioning_callback() {
         val screenWidth = 500
         val screenHeight = 1000
@@ -246,9 +275,9 @@
         assertThat(obtainedParentBounds).isEqualTo(IntBounds(anchorPosition, anchorSize))
         assertThat(obtainedMenuBounds).isEqualTo(
             IntBounds(
-                anchorPosition.x + anchorSize.width + offsetX,
+                anchorPosition.x + offsetX,
                 anchorPosition.y + anchorSize.height + offsetY,
-                anchorPosition.x + anchorSize.width + offsetX + popupSize.width,
+                anchorPosition.x + offsetX + popupSize.width,
                 anchorPosition.y + anchorSize.height + offsetY + popupSize.height
             )
         )
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
index 7f909009..e32b647 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
@@ -159,6 +159,16 @@
     }
 
     @Test
+    fun slider_semantics_focusable() {
+        rule.setMaterialContent {
+            Slider(value = 0f, onValueChange = {}, modifier = Modifier.testTag(tag))
+        }
+
+        rule.onNodeWithTag(tag)
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Focused))
+    }
+
+    @Test
     fun slider_drag() {
         val state = mutableStateOf(0f)
 
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
index 915e930..ca5dee3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
@@ -16,8 +16,8 @@
 
 package androidx.compose.material
 
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
@@ -52,7 +52,7 @@
         rule.setContent {
             scope = rememberCoroutineScope()
             SnackbarHost(hostState) { data ->
-                remember(data) {
+                LaunchedEffect(data) {
                     resultedInvocation += data.message
                     data.dismiss()
                 }
@@ -79,9 +79,9 @@
         rule.setContent {
             scope = rememberCoroutineScope()
             SnackbarHost(hostState) { data ->
-                remember(data) {
+                LaunchedEffect(data) {
                     resultedInvocation += data.message
-                    scope.launch {
+                    launch {
                         delay(30L)
                         data.dismiss()
                     }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
index ba151db..21e4fa0 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
@@ -18,27 +18,41 @@
 
 import androidx.compose.animation.core.AnimationEndReason
 import androidx.compose.animation.core.ManualAnimationClock
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.ScrollableColumn
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.center
+import androidx.compose.ui.test.centerX
+import androidx.compose.ui.test.centerY
+import androidx.compose.ui.test.down
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.moveBy
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performGesture
 import androidx.compose.ui.test.swipe
 import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.test.up
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.milliseconds
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.MediumTest
@@ -1521,6 +1535,223 @@
         }
     }
 
+    @Test
+    fun swipeable_defaultVerticalNestedScrollConnection_nestedDrag() {
+        lateinit var swipeableState: SwipeableState<String>
+        lateinit var anchors: MutableState<Map<Float, String>>
+        lateinit var scrollState: ScrollState
+        rule.setContent {
+            swipeableState = rememberSwipeableState("A")
+            anchors = remember { mutableStateOf(mapOf(0f to "A", -1000f to "B")) }
+            scrollState = rememberScrollState()
+            Box(
+                Modifier
+                    .preferredSize(300.dp)
+                    .nestedScroll(swipeableState.PreUpPostDownNestedScrollConnection)
+                    .swipeable(
+                        state = swipeableState,
+                        anchors = anchors.value,
+                        thresholds = { _, _ -> FractionalThreshold(0.5f) },
+                        orientation = Orientation.Horizontal
+                    )
+            ) {
+                ScrollableColumn(
+                    scrollState = scrollState,
+                    modifier = Modifier.fillMaxWidth().testTag(swipeableTag)
+                ) {
+                    repeat(100) {
+                        Text(text = it.toString(), modifier = Modifier.height(50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("A")
+        }
+
+        rule.onNodeWithTag(swipeableTag)
+            .performGesture {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = -1500f))
+                up()
+            }
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("B")
+            assertThat(scrollState.value).isGreaterThan(0f)
+        }
+
+        rule.onNodeWithTag(swipeableTag)
+            .performGesture {
+                down(Offset(x = 10f, y = 10f))
+                moveBy(Offset(x = 0f, y = 1500f))
+                up()
+            }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("A")
+            assertThat(scrollState.value).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun swipeable_nestedScroll_preFling() {
+        lateinit var swipeableState: SwipeableState<String>
+        lateinit var anchors: MutableState<Map<Float, String>>
+        lateinit var scrollState: ScrollState
+        rule.setContent {
+            swipeableState = rememberSwipeableState("A")
+            anchors = remember { mutableStateOf(mapOf(0f to "A", -1000f to "B")) }
+            scrollState = rememberScrollState()
+            Box(
+                Modifier
+                    .preferredSize(300.dp)
+                    .nestedScroll(swipeableState.PreUpPostDownNestedScrollConnection)
+                    .swipeable(
+                        state = swipeableState,
+                        anchors = anchors.value,
+                        thresholds = { _, _ -> FixedThreshold(56.dp) },
+                        orientation = Orientation.Horizontal
+                    )
+            ) {
+                ScrollableColumn(
+                    scrollState = scrollState,
+                    modifier = Modifier.fillMaxWidth().testTag(swipeableTag)
+                ) {
+                    repeat(100) {
+                        Text(text = it.toString(), modifier = Modifier.height(50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("A")
+        }
+
+        rule.onNodeWithTag(swipeableTag)
+            .performGesture {
+                swipeWithVelocity(
+                    center,
+                    center.copy(y = centerY - 500, x = centerX),
+                    duration = 50.milliseconds,
+                    endVelocity = 20000f
+                )
+            }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("B")
+            // should eat all velocity, no internal scroll
+            assertThat(scrollState.value).isEqualTo(0f)
+        }
+
+        rule.onNodeWithTag(swipeableTag)
+            .performGesture {
+                swipeWithVelocity(
+                    center,
+                    center.copy(y = centerY + 500, x = centerX),
+                    duration = 50.milliseconds,
+                    endVelocity = 20000f
+                )
+            }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("A")
+            assertThat(scrollState.value).isEqualTo(0f)
+        }
+    }
+
+    @Test
+    fun swipeable_nestedScroll_postFlings() {
+        lateinit var swipeableState: SwipeableState<String>
+        lateinit var anchors: MutableState<Map<Float, String>>
+        lateinit var scrollState: ScrollState
+        rule.setContent {
+            swipeableState = rememberSwipeableState("B")
+            anchors = remember { mutableStateOf(mapOf(0f to "A", -1000f to "B")) }
+            scrollState = rememberScrollState(initial = 5000f)
+            Box(
+                Modifier
+                    .preferredSize(300.dp)
+                    .nestedScroll(swipeableState.PreUpPostDownNestedScrollConnection)
+                    .swipeable(
+                        state = swipeableState,
+                        anchors = anchors.value,
+                        thresholds = { _, _ -> FixedThreshold(56.dp) },
+                        orientation = Orientation.Horizontal
+                    )
+            ) {
+                ScrollableColumn(
+                    scrollState = scrollState,
+                    modifier = Modifier.fillMaxWidth().testTag(swipeableTag)
+                ) {
+                    repeat(100) {
+                        Text(text = it.toString(), modifier = Modifier.height(50.dp))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("B")
+            assertThat(scrollState.value).isEqualTo(5000f)
+        }
+
+        rule.onNodeWithTag(swipeableTag)
+            .performGesture {
+                // swipe less than scrollState.value but with velocity to test that backdrop won't
+                // move when receives, because it's at anchor
+                swipeWithVelocity(
+                    center,
+                    center.copy(y = centerY + 1500, x = centerX),
+                    duration = 50.milliseconds,
+                    endVelocity = 20000f
+                )
+            }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("B")
+            assertThat(scrollState.value).isEqualTo(0f)
+            // set value again to test overshoot
+            scrollState.scrollBy(500f)
+        }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("B")
+            assertThat(scrollState.value).isEqualTo(500f)
+        }
+
+        rule.onNodeWithTag(swipeableTag)
+            .performGesture {
+                // swipe more than scrollState.value so backdrop start receiving nested scroll
+                swipeWithVelocity(
+                    center,
+                    center.copy(y = centerY + 1500, x = centerX),
+                    duration = 50.milliseconds,
+                    endVelocity = 20000f
+                )
+            }
+
+        advanceClock()
+
+        rule.runOnIdle {
+            assertThat(swipeableState.value).isEqualTo("A")
+            assertThat(scrollState.value).isEqualTo(0f)
+        }
+    }
+
     private fun swipeRight(
         offset: Float = 100f,
         velocity: Float? = null
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
index 9cf48ed..9b05fd8 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
@@ -98,7 +98,7 @@
                 Switch(
                     checked = true,
                     onCheckedChange = { },
-                    colors = SwitchConstants.defaultColors(checkedThumbColor = Color.Red)
+                    colors = SwitchDefaults.colors(checkedThumbColor = Color.Red)
                 )
             }
         }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
index a10d1cb..bd3d988 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
@@ -19,7 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.preferredHeight
-import androidx.compose.material.TabConstants.defaultTabIndicatorOffset
+import androidx.compose.material.TabDefaults.tabIndicatorOffset
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material.samples.ScrollingTextTabs
@@ -124,7 +124,7 @@
             val indicator = @Composable { tabPositions: List<TabPosition> ->
                 Box(
                     Modifier
-                        .defaultTabIndicatorOffset(tabPositions[state])
+                        .tabIndicatorOffset(tabPositions[state])
                         .fillMaxWidth()
                         .preferredHeight(indicatorHeight)
                         .background(color = Color.Red)
@@ -299,7 +299,7 @@
             val indicator = @Composable { tabPositions: List<TabPosition> ->
                 Box(
                     Modifier
-                        .defaultTabIndicatorOffset(tabPositions[state])
+                        .tabIndicatorOffset(tabPositions[state])
                         .fillMaxWidth()
                         .preferredHeight(indicatorHeight)
                         .background(color = Color.Red)
@@ -330,7 +330,7 @@
         rule.onNodeWithTag("indicator")
             .assertPositionInRootIsEqualTo(
                 // Tabs in a scrollable tab row are offset 52.dp from each end
-                expectedLeft = TabConstants.DefaultScrollableTabRowPadding,
+                expectedLeft = TabDefaults.ScrollableTabRowPadding,
                 expectedTop = tabRowBounds.height - indicatorHeight
             )
 
@@ -341,7 +341,7 @@
         // should be in the middle of the TabRow
         rule.onNodeWithTag("indicator")
             .assertPositionInRootIsEqualTo(
-                expectedLeft = TabConstants.DefaultScrollableTabRowPadding + minimumTabWidth,
+                expectedLeft = TabDefaults.ScrollableTabRowPadding + minimumTabWidth,
                 expectedTop = tabRowBounds.height - indicatorHeight
             )
     }
@@ -446,8 +446,8 @@
     fun testInspectorValue() {
         val pos = TabPosition(10.0.dp, 200.0.dp)
         rule.setContent {
-            val modifier = Modifier.defaultTabIndicatorOffset(pos) as InspectableValue
-            assertThat(modifier.nameFallback).isEqualTo("defaultTabIndicatorOffset")
+            val modifier = Modifier.tabIndicatorOffset(pos) as InspectableValue
+            assertThat(modifier.nameFallback).isEqualTo("tabIndicatorOffset")
             assertThat(modifier.valueOverride).isEqualTo(pos)
             assertThat(modifier.inspectableElements.asIterable()).isEmpty()
         }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index 80d88be..2572c0f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -36,9 +36,8 @@
 import androidx.compose.runtime.remember
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
@@ -81,7 +80,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
 class OutlinedTextFieldTest {
     private val ExpectedMinimumTextFieldHeight = 56.dp
     private val ExpectedPadding = 16.dp
@@ -105,7 +104,7 @@
                 OutlinedTextField(
                     modifier = Modifier
                         .testTag(textField1Tag)
-                        .focusObserver { textField1Focused = it.isFocused },
+                        .onFocusChanged { textField1Focused = it.isFocused },
                     value = "input1",
                     onValueChange = {},
                     label = {}
@@ -113,7 +112,7 @@
                 OutlinedTextField(
                     modifier = Modifier
                         .testTag(textField2Tag)
-                        .focusObserver { textField2Focused = it.isFocused },
+                        .onFocusChanged { textField2Focused = it.isFocused },
                     value = "input2",
                     onValueChange = {},
                     label = {}
@@ -144,7 +143,7 @@
                 OutlinedTextField(
                     modifier = Modifier
                         .testTag(TextfieldTag)
-                        .focusObserver { focused = it.isFocused },
+                        .onFocusChanged { focused = it.isFocused },
                     value = "input",
                     onValueChange = {},
                     label = {}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index 8b2d1f3..e5d75e3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -42,10 +42,9 @@
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
@@ -95,7 +94,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
 class TextFieldTest {
 
     private val ExpectedMinimumTextFieldHeight = 56.dp
@@ -134,7 +133,7 @@
             Column {
                 TextField(
                     modifier = Modifier
-                        .focusObserver { textField1Focused = it.isFocused }
+                        .onFocusChanged { textField1Focused = it.isFocused }
                         .testTag(textField1Tag),
                     value = "input1",
                     onValueChange = {},
@@ -142,7 +141,7 @@
                 )
                 TextField(
                     modifier = Modifier
-                        .focusObserver { textField2Focused = it.isFocused }
+                        .onFocusChanged { textField2Focused = it.isFocused }
                         .testTag(textField2Tag),
                     value = "input2",
                     onValueChange = {},
@@ -173,7 +172,7 @@
             Box {
                 TextField(
                     modifier = Modifier
-                        .focusObserver { focused = it.isFocused }
+                        .onFocusChanged { focused = it.isFocused }
                         .testTag(TextfieldTag),
                     value = "input",
                     onValueChange = {},
@@ -194,8 +193,7 @@
 
     @Test
     fun testTextField_showHideKeyboardBasedOnFocus() {
-        val parentFocusRequester = FocusRequester()
-        val focusRequester = FocusRequester()
+        val (focusRequester, parentFocusRequester) = FocusRequester.createRefs()
         lateinit var hostView: View
         rule.setMaterialContent {
             hostView = AmbientView.current
@@ -224,8 +222,7 @@
 
     @Test
     fun testTextField_clickingOnTextAfterDismissingKeyboard_showsKeyboard() {
-        val parentFocusRequester = FocusRequester()
-        val focusRequester = FocusRequester()
+        val (focusRequester, parentFocusRequester) = FocusRequester.createRefs()
         lateinit var softwareKeyboardController: SoftwareKeyboardController
         lateinit var hostView: View
         rule.setMaterialContent {
@@ -823,7 +820,7 @@
             Box(Modifier.background(color = Color.White)) {
                 TextField(
                     modifier = Modifier
-                        .focusObserver { if (it.isFocused) latch.countDown() }
+                        .onFocusChanged { if (it.isFocused) latch.countDown() }
                         .testTag(TextfieldTag),
                     value = "",
                     onValueChange = {},
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
index 4d4bad92..682db4a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
@@ -23,8 +23,8 @@
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
@@ -38,6 +38,7 @@
 import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.graphics.Color
@@ -48,6 +49,7 @@
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.offset
 import androidx.compose.ui.util.fastForEach
@@ -86,7 +88,7 @@
 class BackdropScaffoldState(
     initialValue: BackdropValue,
     clock: AnimationClockObservable,
-    animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
     confirmStateChange: (BackdropValue) -> Boolean = { true },
     val snackbarHostState: SnackbarHostState = SnackbarHostState()
 ) : SwipeableState<BackdropValue>(
@@ -139,6 +141,8 @@
         )
     }
 
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
     companion object {
         /**
          * The default [Saver] implementation for [BackdropScaffoldState].
@@ -177,7 +181,7 @@
 fun rememberBackdropScaffoldState(
     initialValue: BackdropValue,
     clock: AnimationClockObservable = AmbientAnimationClock.current,
-    animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
     confirmStateChange: (BackdropValue) -> Boolean = { true },
     snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
 ): BackdropScaffoldState {
@@ -268,17 +272,17 @@
     modifier: Modifier = Modifier,
     scaffoldState: BackdropScaffoldState = rememberBackdropScaffoldState(Concealed),
     gesturesEnabled: Boolean = true,
-    peekHeight: Dp = BackdropScaffoldConstants.DefaultPeekHeight,
-    headerHeight: Dp = BackdropScaffoldConstants.DefaultHeaderHeight,
+    peekHeight: Dp = BackdropScaffoldDefaults.PeekHeight,
+    headerHeight: Dp = BackdropScaffoldDefaults.HeaderHeight,
     persistentAppBar: Boolean = true,
     stickyFrontLayer: Boolean = true,
     backLayerBackgroundColor: Color = MaterialTheme.colors.primary,
     backLayerContentColor: Color = contentColorFor(backLayerBackgroundColor),
-    frontLayerShape: Shape = BackdropScaffoldConstants.DefaultFrontLayerShape,
-    frontLayerElevation: Dp = BackdropScaffoldConstants.DefaultFrontLayerElevation,
+    frontLayerShape: Shape = BackdropScaffoldDefaults.frontLayerShape,
+    frontLayerElevation: Dp = BackdropScaffoldDefaults.FrontLayerElevation,
     frontLayerBackgroundColor: Color = MaterialTheme.colors.surface,
     frontLayerContentColor: Color = contentColorFor(frontLayerBackgroundColor),
-    frontLayerScrimColor: Color = BackdropScaffoldConstants.DefaultFrontLayerScrimColor,
+    frontLayerScrimColor: Color = BackdropScaffoldDefaults.frontLayerScrimColor,
     snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
     appBar: @Composable () -> Unit,
     backLayerContent: @Composable () -> Unit,
@@ -317,19 +321,23 @@
                 revealedHeight = min(revealedHeight, backLayerHeight)
             }
 
-            val swipeable = Modifier.swipeable(
-                state = scaffoldState,
-                anchors = mapOf(
-                    peekHeightPx to Concealed,
-                    revealedHeight to Revealed
-                ),
-                orientation = Orientation.Vertical,
-                enabled = gesturesEnabled
-            )
+            val swipeable = Modifier
+                .nestedScroll(scaffoldState.nestedScrollConnection)
+                .swipeable(
+                    state = scaffoldState,
+                    anchors = mapOf(
+                        peekHeightPx to Concealed,
+                        revealedHeight to Revealed
+                    ),
+                    orientation = Orientation.Vertical,
+                    enabled = gesturesEnabled
+                )
 
             // Front layer
             Surface(
-                Modifier.offset(y = { scaffoldState.offset.value }).then(swipeable),
+                Modifier
+                    .offset { IntOffset(0, scaffoldState.offset.value.roundToInt()) }
+                    .then(swipeable),
                 shape = frontLayerShape,
                 elevation = frontLayerElevation,
                 color = frontLayerBackgroundColor,
@@ -461,6 +469,13 @@
 /**
  * Contains useful constants for [BackdropScaffold].
  */
+@Deprecated(
+    "BackdropScaffoldConstants has been replaced with BackdropScaffoldDefaults",
+    ReplaceWith(
+        "BackdropScaffoldDefaults",
+        "androidx.compose.material.BackdropScaffoldDefaults"
+    )
+)
 object BackdropScaffoldConstants {
 
     /**
@@ -476,8 +491,8 @@
     /**
      * The default shape of the front layer.
      */
-    @Composable
     val DefaultFrontLayerShape: Shape
+        @Composable
         get() = MaterialTheme.shapes.large
             .copy(topLeft = CornerSize(16.dp), topRight = CornerSize(16.dp))
 
@@ -489,9 +504,43 @@
     /**
      * The default color of the scrim applied to the front layer.
      */
-    @Composable
     val DefaultFrontLayerScrimColor: Color
-        get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
+        @Composable get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
+}
+
+/**
+ * Contains useful defaults for [BackdropScaffold].
+ */
+object BackdropScaffoldDefaults {
+
+    /**
+     * The default peek height of the back layer.
+     */
+    val PeekHeight = 56.dp
+
+    /**
+     * The default header height of the front layer.
+     */
+    val HeaderHeight = 48.dp
+
+    /**
+     * The default shape of the front layer.
+     */
+    val frontLayerShape: Shape
+        @Composable
+        get() = MaterialTheme.shapes.large
+            .copy(topLeft = CornerSize(16.dp), topRight = CornerSize(16.dp))
+
+    /**
+     * The default elevation of the front layer.
+     */
+    val FrontLayerElevation = 1.dp
+
+    /**
+     * The default color of the scrim applied to the front layer.
+     */
+    val frontLayerScrimColor: Color
+        @Composable get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
 }
 
 private val AnimationSlideOffset = 20.dp
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
index 5cfd7af..30d12b7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
@@ -30,7 +30,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.emptyContent
@@ -153,7 +153,7 @@
     // The color of the Ripple should always the selected color, as we want to show the color
     // before the item is considered selected, and hence before the new contentColor is
     // provided by BottomNavigationTransition.
-    val ripple = rememberRippleIndication(bounded = false, color = selectedContentColor)
+    val ripple = rememberRipple(bounded = false, color = selectedContentColor)
 
     // TODO This composable has magic behavior within a Row; reconsider this behavior later
     Box(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index ba1e9b6..c210065 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -37,12 +37,13 @@
 import androidx.compose.runtime.savedinstancestate.Saver
 import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.WithConstraints
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.WithConstraints
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.platform.AmbientDensity
@@ -79,7 +80,7 @@
 class BottomSheetState(
     initialValue: BottomSheetValue,
     clock: AnimationClockObservable,
-    animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
     confirmStateChange: (BottomSheetValue) -> Boolean = { true }
 ) : SwipeableState<BottomSheetValue>(
     initialValue = initialValue,
@@ -151,6 +152,8 @@
             }
         )
     }
+
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
 }
 
 /**
@@ -164,7 +167,7 @@
 @ExperimentalMaterialApi
 fun rememberBottomSheetState(
     initialValue: BottomSheetValue,
-    animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
     confirmStateChange: (BottomSheetValue) -> Boolean = { true }
 ): BottomSheetState {
     val disposableClock = AmbientAnimationClock.current.asDisposableClock()
@@ -279,17 +282,17 @@
     floatingActionButtonPosition: FabPosition = FabPosition.End,
     sheetGesturesEnabled: Boolean = true,
     sheetShape: Shape = MaterialTheme.shapes.large,
-    sheetElevation: Dp = BottomSheetScaffoldConstants.DefaultSheetElevation,
+    sheetElevation: Dp = BottomSheetScaffoldDefaults.SheetElevation,
     sheetBackgroundColor: Color = MaterialTheme.colors.surface,
     sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
-    sheetPeekHeight: Dp = BottomSheetScaffoldConstants.DefaultSheetPeekHeight,
+    sheetPeekHeight: Dp = BottomSheetScaffoldDefaults.SheetPeekHeight,
     drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
     drawerGesturesEnabled: Boolean = true,
     drawerShape: Shape = MaterialTheme.shapes.large,
-    drawerElevation: Dp = DrawerConstants.DefaultElevation,
+    drawerElevation: Dp = DrawerDefaults.Elevation,
     drawerBackgroundColor: Color = MaterialTheme.colors.surface,
     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
-    drawerScrimColor: Color = DrawerConstants.defaultScrimColor,
+    drawerScrimColor: Color = DrawerDefaults.scrimColor,
     backgroundColor: Color = MaterialTheme.colors.background,
     contentColor: Color = contentColorFor(backgroundColor),
     bodyContent: @Composable (PaddingValues) -> Unit
@@ -299,16 +302,18 @@
         val peekHeightPx = with(AmbientDensity.current) { sheetPeekHeight.toPx() }
         var bottomSheetHeight by remember { mutableStateOf(fullHeight) }
 
-        val swipeable = Modifier.swipeable(
-            state = scaffoldState.bottomSheetState,
-            anchors = mapOf(
-                fullHeight - peekHeightPx to BottomSheetValue.Collapsed,
-                fullHeight - bottomSheetHeight to BottomSheetValue.Expanded
-            ),
-            orientation = Orientation.Vertical,
-            enabled = sheetGesturesEnabled,
-            resistance = null
-        )
+        val swipeable = Modifier
+            .nestedScroll(scaffoldState.bottomSheetState.nestedScrollConnection)
+            .swipeable(
+                state = scaffoldState.bottomSheetState,
+                anchors = mapOf(
+                    fullHeight - peekHeightPx to BottomSheetValue.Collapsed,
+                    fullHeight - bottomSheetHeight to BottomSheetValue.Expanded
+                ),
+                orientation = Orientation.Vertical,
+                enabled = sheetGesturesEnabled,
+                resistance = null
+            )
 
         val child = @Composable {
             BottomSheetScaffoldStack(
@@ -422,6 +427,13 @@
 /**
  * Contains useful constants for [BottomSheetScaffold].
  */
+@Deprecated(
+    message = "BottomSheetScaffoldConstants has been replaced with BottomSheetScaffoldDefaults",
+    ReplaceWith(
+        "BottomSheetScaffoldDefaults",
+        "androidx.compose.material.BottomSheetScaffoldDefaults"
+    )
+)
 object BottomSheetScaffoldConstants {
 
     /**
@@ -433,4 +445,20 @@
      * The default peek height used by [BottomSheetScaffold].
      */
     val DefaultSheetPeekHeight = 56.dp
+}
+
+/**
+ * Contains useful defaults for [BottomSheetScaffold].
+ */
+object BottomSheetScaffoldDefaults {
+
+    /**
+     * The default elevation used by [BottomSheetScaffold].
+     */
+    val SheetElevation = 8.dp
+
+    /**
+     * The default peek height used by [BottomSheetScaffold].
+     */
+    val SheetPeekHeight = 56.dp
 }
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
index cdeff5d..b23e6fe 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
@@ -19,10 +19,10 @@
 package androidx.compose.material
 
 import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.VectorConverter
 import androidx.compose.animation.asDisposableClock
 import androidx.compose.animation.core.AnimationClockObservable
 import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
 import androidx.compose.foundation.AmbientIndication
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.Interaction
@@ -82,11 +82,11 @@
  * it is [Interaction.Pressed].
  * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
  * states. This controls the size of the shadow below the button. Pass `null` here to disable
- * elevation for this button. See [ButtonConstants.defaultElevation].
+ * elevation for this button. See [ButtonDefaults.elevation].
  * @param shape Defines the button's shape as well as its shadow
  * @param border Border to draw around the button
  * @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonConstants.defaultButtonColors].
+ * this button in different states. See [ButtonDefaults.buttonColors].
  * @param contentPadding The spacing values to apply internally between the container and the content
  */
 @OptIn(ExperimentalMaterialApi::class)
@@ -96,11 +96,11 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionState: InteractionState = remember { InteractionState() },
-    elevation: ButtonElevation? = ButtonConstants.defaultElevation(),
+    elevation: ButtonElevation? = ButtonDefaults.elevation(),
     shape: Shape = MaterialTheme.shapes.small,
     border: BorderStroke? = null,
-    colors: ButtonColors = ButtonConstants.defaultButtonColors(),
-    contentPadding: PaddingValues = ButtonConstants.DefaultContentPadding,
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
+    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
     content: @Composable RowScope.() -> Unit
 ) {
     // TODO(aelias): Avoid manually putting the clickable above the clip and
@@ -128,8 +128,8 @@
                 Row(
                     Modifier
                         .defaultMinSizeConstraints(
-                            minWidth = ButtonConstants.DefaultMinWidth,
-                            minHeight = ButtonConstants.DefaultMinHeight
+                            minWidth = ButtonDefaults.MinWidth,
+                            minHeight = ButtonDefaults.MinHeight
                         )
                         .indication(interactionState, AmbientIndication.current())
                         .padding(contentPadding),
@@ -176,7 +176,7 @@
  * @param shape Defines the button's shape as well as its shadow
  * @param border Border to draw around the button
  * @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonConstants.defaultOutlinedButtonColors].
+ * this button in different states. See [ButtonDefaults.outlinedButtonColors].
  * @param contentPadding The spacing values to apply internally between the container and the content
  */
 @OptIn(ExperimentalMaterialApi::class)
@@ -188,9 +188,9 @@
     interactionState: InteractionState = remember { InteractionState() },
     elevation: ButtonElevation? = null,
     shape: Shape = MaterialTheme.shapes.small,
-    border: BorderStroke? = ButtonConstants.defaultOutlinedBorder,
-    colors: ButtonColors = ButtonConstants.defaultOutlinedButtonColors(),
-    contentPadding: PaddingValues = ButtonConstants.DefaultContentPadding,
+    border: BorderStroke? = ButtonDefaults.outlinedBorder,
+    colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
+    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
     noinline content: @Composable RowScope.() -> Unit
 ) = Button(
     onClick = onClick,
@@ -236,7 +236,7 @@
  * @param shape Defines the button's shape as well as its shadow
  * @param border Border to draw around the button
  * @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonConstants.defaultTextButtonColors].
+ * this button in different states. See [ButtonDefaults.textButtonColors].
  * @param contentPadding The spacing values to apply internally between the container and the content
  */
 @OptIn(ExperimentalMaterialApi::class)
@@ -249,8 +249,8 @@
     elevation: ButtonElevation? = null,
     shape: Shape = MaterialTheme.shapes.small,
     border: BorderStroke? = null,
-    colors: ButtonColors = ButtonConstants.defaultTextButtonColors(),
-    contentPadding: PaddingValues = ButtonConstants.DefaultTextContentPadding,
+    colors: ButtonColors = ButtonDefaults.textButtonColors(),
+    contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
     noinline content: @Composable RowScope.() -> Unit
 ) = Button(
     onClick = onClick,
@@ -268,7 +268,7 @@
 /**
  * Represents the elevation for a button in different states.
  *
- * See [ButtonConstants.defaultElevation] for the default elevation used in a [Button].
+ * See [ButtonDefaults.elevation] for the default elevation used in a [Button].
  */
 @ExperimentalMaterialApi
 @Stable
@@ -285,10 +285,10 @@
 /**
  * Represents the background and content colors used in a button in different states.
  *
- * See [ButtonConstants.defaultButtonColors] for the default colors used in a [Button].
- * See [ButtonConstants.defaultOutlinedButtonColors] for the default colors used in a
+ * See [ButtonDefaults.buttonColors] for the default colors used in a [Button].
+ * See [ButtonDefaults.outlinedButtonColors] for the default colors used in a
  * [OutlinedButton].
- * See [ButtonConstants.defaultTextButtonColors] for the default colors used in a [TextButton].
+ * See [ButtonDefaults.textButtonColors] for the default colors used in a [TextButton].
  */
 @ExperimentalMaterialApi
 @Stable
@@ -311,6 +311,13 @@
 /**
  * Contains the default values used by [Button]
  */
+@Deprecated(
+    "ButtonConstants has been replaced with ButtonDefaults",
+    ReplaceWith(
+        "ButtonDefaults",
+        "androidx.compose.material.ButtonDefaults"
+    )
+)
 object ButtonConstants {
     private val ButtonHorizontalPadding = 16.dp
     private val ButtonVerticalPadding = 8.dp
@@ -364,6 +371,13 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "ButtonConstants has been replaced with ButtonDefaults",
+        ReplaceWith(
+            "ButtonDefaults.elevation(elevation, pressedElevation, disabledElevation)",
+            "androidx.compose.material.ButtonDefaults"
+        )
+    )
     fun defaultElevation(
         defaultElevation: Dp = 2.dp,
         pressedElevation: Dp = 8.dp,
@@ -393,6 +407,14 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "ButtonConstants has been replaced with ButtonDefaults",
+        ReplaceWith(
+            "ButtonDefaults.buttonColors(backgroundColor, disabledBackgroundColor, contentColor, " +
+                "disabledContentColor)",
+            "androidx.compose.material.ButtonDefaults"
+        )
+    )
     fun defaultButtonColors(
         backgroundColor: Color = MaterialTheme.colors.primary,
         disabledBackgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
@@ -417,6 +439,14 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "ButtonConstants has been replaced with ButtonDefaults",
+        ReplaceWith(
+            "ButtonDefaults.outlinedButtonColors(backgroundColor, disabledBackgroundColor, " +
+                "contentColor, disabledContentColor)",
+            "androidx.compose.material.ButtonDefaults"
+        )
+    )
     fun defaultOutlinedButtonColors(
         backgroundColor: Color = MaterialTheme.colors.surface,
         contentColor: Color = MaterialTheme.colors.primary,
@@ -439,6 +469,14 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "ButtonConstants has been replaced with ButtonDefaults",
+        ReplaceWith(
+            "ButtonDefaults.textButtonColors(backgroundColor, disabledBackgroundColor, " +
+                "contentColor, disabledContentColor)",
+            "androidx.compose.material.ButtonDefaults"
+        )
+    )
     fun defaultTextButtonColors(
         backgroundColor: Color = Color.Transparent,
         contentColor: Color = MaterialTheme.colors.primary,
@@ -464,8 +502,8 @@
     /**
      * The default disabled content color used by all types of [Button]s
      */
-    @Composable
     val defaultOutlinedBorder: BorderStroke
+        @Composable
         get() = BorderStroke(
             OutlinedBorderSize, MaterialTheme.colors.onSurface.copy(alpha = OutlinedBorderOpacity)
         )
@@ -482,6 +520,179 @@
 }
 
 /**
+ * Contains the default values used by [Button]
+ */
+object ButtonDefaults {
+    private val ButtonHorizontalPadding = 16.dp
+    private val ButtonVerticalPadding = 8.dp
+
+    /**
+     * The default content padding used by [Button]
+     */
+    val ContentPadding = PaddingValues(
+        start = ButtonHorizontalPadding,
+        top = ButtonVerticalPadding,
+        end = ButtonHorizontalPadding,
+        bottom = ButtonVerticalPadding
+    )
+
+    /**
+     * The default min width applied for the [Button].
+     * Note that you can override it by applying Modifier.widthIn directly on [Button].
+     */
+    val MinWidth = 64.dp
+
+    /**
+     * The default min width applied for the [Button].
+     * Note that you can override it by applying Modifier.heightIn directly on [Button].
+     */
+    val MinHeight = 36.dp
+
+    /**
+     * The default size of the icon when used inside a [Button].
+     *
+     * @sample androidx.compose.material.samples.ButtonWithIconSample
+     */
+    val IconSize = 18.dp
+
+    /**
+     * The default size of the spacing between an icon and a text when they used inside a [Button].
+     *
+     * @sample androidx.compose.material.samples.ButtonWithIconSample
+     */
+    val IconSpacing = 8.dp
+
+    // TODO: b/152525426 add support for focused and hovered states
+    /**
+     * Creates a [ButtonElevation] that will animate between the provided values according to the
+     * Material specification for a [Button].
+     *
+     * @param defaultElevation the elevation to use when the [Button] is enabled, and has no
+     * other [Interaction]s.
+     * @param pressedElevation the elevation to use when the [Button] is enabled and
+     * is [Interaction.Pressed].
+     * @param disabledElevation the elevation to use when the [Button] is not enabled.
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun elevation(
+        defaultElevation: Dp = 2.dp,
+        pressedElevation: Dp = 8.dp,
+        // focused: Dp = 4.dp,
+        // hovered: Dp = 4.dp,
+        disabledElevation: Dp = 0.dp
+    ): ButtonElevation {
+        val clock = AmbientAnimationClock.current.asDisposableClock()
+        return remember(defaultElevation, pressedElevation, disabledElevation, clock) {
+            DefaultButtonElevation(
+                defaultElevation = defaultElevation,
+                pressedElevation = pressedElevation,
+                disabledElevation = disabledElevation,
+                clock = clock
+            )
+        }
+    }
+
+    /**
+     * Creates a [ButtonColors] that represents the default background and content colors used in
+     * a [Button].
+     *
+     * @param backgroundColor the background color of this [Button] when enabled
+     * @param disabledBackgroundColor the background color of this [Button] when not enabled
+     * @param contentColor the content color of this [Button] when enabled
+     * @param disabledContentColor the content color of this [Button] when not enabled
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun buttonColors(
+        backgroundColor: Color = MaterialTheme.colors.primary,
+        disabledBackgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
+            .compositeOver(MaterialTheme.colors.surface),
+        contentColor: Color = contentColorFor(backgroundColor),
+        disabledContentColor: Color = MaterialTheme.colors.onSurface
+            .copy(alpha = ContentAlpha.disabled)
+    ): ButtonColors = DefaultButtonColors(
+        backgroundColor,
+        disabledBackgroundColor,
+        contentColor,
+        disabledContentColor
+    )
+
+    /**
+     * Creates a [ButtonColors] that represents the default background and content colors used in
+     * an [OutlinedButton].
+     *
+     * @param backgroundColor the background color of this [OutlinedButton]
+     * @param contentColor the content color of this [OutlinedButton] when enabled
+     * @param disabledContentColor the content color of this [OutlinedButton] when not enabled
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun outlinedButtonColors(
+        backgroundColor: Color = MaterialTheme.colors.surface,
+        contentColor: Color = MaterialTheme.colors.primary,
+        disabledContentColor: Color = MaterialTheme.colors.onSurface
+            .copy(alpha = ContentAlpha.disabled)
+    ): ButtonColors = DefaultButtonColors(
+        backgroundColor,
+        backgroundColor,
+        contentColor,
+        disabledContentColor
+    )
+
+    /**
+     * Creates a [ButtonColors] that represents the default background and content colors used in
+     * a [TextButton].
+     *
+     * @param backgroundColor the background color of this [TextButton]
+     * @param contentColor the content color of this [TextButton] when enabled
+     * @param disabledContentColor the content color of this [TextButton] when not enabled
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun textButtonColors(
+        backgroundColor: Color = Color.Transparent,
+        contentColor: Color = MaterialTheme.colors.primary,
+        disabledContentColor: Color = MaterialTheme.colors.onSurface
+            .copy(alpha = ContentAlpha.disabled)
+    ): ButtonColors = DefaultButtonColors(
+        backgroundColor,
+        backgroundColor,
+        contentColor,
+        disabledContentColor
+    )
+
+    /**
+     * The default color opacity used for an [OutlinedButton]'s border color
+     */
+    const val OutlinedBorderOpacity = 0.12f
+
+    /**
+     * The default [OutlinedButton]'s border size
+     */
+    val OutlinedBorderSize = 1.dp
+
+    /**
+     * The default disabled content color used by all types of [Button]s
+     */
+    val outlinedBorder: BorderStroke
+        @Composable
+        get() = BorderStroke(
+            OutlinedBorderSize, MaterialTheme.colors.onSurface.copy(alpha = OutlinedBorderOpacity)
+        )
+
+    private val TextButtonHorizontalPadding = 8.dp
+
+    /**
+     * The default content padding used by [TextButton]
+     */
+    val TextButtonContentPadding = ContentPadding.copy(
+        start = TextButtonHorizontalPadding,
+        end = TextButtonHorizontalPadding
+    )
+}
+
+/**
  * Default [ButtonElevation] implementation.
  */
 @OptIn(ExperimentalMaterialApi::class)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
index 1ae6d72..811d694 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
@@ -35,7 +35,7 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.triStateToggleable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
@@ -76,7 +76,7 @@
  * [InteractionState] if you want to read the [InteractionState] and customize the appearance /
  * behavior of this Checkbox in different [Interaction]s.
  * @param colors [CheckboxColors] that will be used to determine the color of the checkmark / box
- * / border in different states. See [CheckboxConstants.defaultColors].
+ * / border in different states. See [CheckboxDefaults.colors].
  */
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
@@ -86,7 +86,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionState: InteractionState = remember { InteractionState() },
-    colors: CheckboxColors = CheckboxConstants.defaultColors()
+    colors: CheckboxColors = CheckboxDefaults.colors()
 ) {
     TriStateCheckbox(
         state = ToggleableState(checked),
@@ -120,7 +120,7 @@
  * [InteractionState] if you want to read the [InteractionState] and customize the appearance /
  * behavior of this TriStateCheckbox in different [Interaction]s.
  * @param colors [CheckboxColors] that will be used to determine the color of the checkmark / box
- * / border in different states. See [CheckboxConstants.defaultColors].
+ * / border in different states. See [CheckboxDefaults.colors].
  */
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
@@ -130,7 +130,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionState: InteractionState = remember { InteractionState() },
-    colors: CheckboxColors = CheckboxConstants.defaultColors()
+    colors: CheckboxColors = CheckboxDefaults.colors()
 ) {
     CheckboxImpl(
         enabled = enabled,
@@ -141,7 +141,7 @@
                 onClick = onClick,
                 enabled = enabled,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(
+                indication = rememberRipple(
                     bounded = false,
                     radius = CheckboxRippleRadius
                 )
@@ -155,7 +155,7 @@
  * Represents the colors used by the three different sections (checkmark, box, and border) of a
  * [Checkbox] or [TriStateCheckbox] in different states.
  *
- * See [CheckboxConstants.defaultColors] for the default implementation that follows Material
+ * See [CheckboxDefaults.colors] for the default implementation that follows Material
  * specifications.
  */
 @ExperimentalMaterialApi
@@ -190,6 +190,13 @@
 /**
  * Constants used in [Checkbox] and [TriStateCheckbox].
  */
+@Deprecated(
+    "CheckboxConstants has been replaced with CheckboxDefaults",
+    ReplaceWith(
+        "CheckboxDefaults",
+        "androidx.compose.material.CheckboxDefaults"
+    )
+)
 object CheckboxConstants {
     /**
      * Creates a [CheckboxColors] that will animate between the provided colors according to the
@@ -204,6 +211,14 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "CheckboxConstants has been replaced with CheckboxDefaults",
+        ReplaceWith(
+            "CheckboxDefaults.colors(checkedColor, uncheckedColor, checkmarkColor, disabledColor," +
+                " disabledIndeterminateColor)",
+            "androidx.compose.material.CheckboxDefaults"
+        )
+    )
     fun defaultColors(
         checkedColor: Color = MaterialTheme.colors.secondary,
         uncheckedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
@@ -238,6 +253,57 @@
     }
 }
 
+/**
+ * Defaults used in [Checkbox] and [TriStateCheckbox].
+ */
+object CheckboxDefaults {
+    /**
+     * Creates a [CheckboxColors] that will animate between the provided colors according to the
+     * Material specification.
+     *
+     * @param checkedColor the color that will be used for the border and box when checked
+     * @param uncheckedColor color that will be used for the border when unchecked
+     * @param checkmarkColor color that will be used for the checkmark when checked
+     * @param disabledColor color that will be used for the box and border when disabled
+     * @param disabledIndeterminateColor color that will be used for the box and
+     * border in a [TriStateCheckbox] when disabled AND in an [ToggleableState.Indeterminate] state.
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun colors(
+        checkedColor: Color = MaterialTheme.colors.secondary,
+        uncheckedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
+        checkmarkColor: Color = MaterialTheme.colors.surface,
+        disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
+        disabledIndeterminateColor: Color = checkedColor.copy(alpha = ContentAlpha.disabled)
+    ): CheckboxColors {
+        val clock = AmbientAnimationClock.current.asDisposableClock()
+        return remember(
+            checkedColor,
+            uncheckedColor,
+            checkmarkColor,
+            disabledColor,
+            disabledIndeterminateColor,
+            clock
+        ) {
+            DefaultCheckboxColors(
+                checkedBorderColor = checkedColor,
+                checkedBoxColor = checkedColor,
+                checkedCheckmarkColor = checkmarkColor,
+                uncheckedCheckmarkColor = checkmarkColor.copy(alpha = 0f),
+                uncheckedBoxColor = checkedColor.copy(alpha = 0f),
+                disabledCheckedBoxColor = disabledColor,
+                disabledUncheckedBoxColor = disabledColor.copy(alpha = 0f),
+                disabledIndeterminateBoxColor = disabledIndeterminateColor,
+                uncheckedBorderColor = uncheckedColor,
+                disabledBorderColor = disabledColor,
+                disabledIndeterminateBorderColor = disabledIndeterminateColor,
+                clock = clock
+            )
+        }
+    }
+}
+
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
 private fun CheckboxImpl(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt
index f4e8a3b..1796da7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt
@@ -31,8 +31,8 @@
      * A high level of content alpha, used to represent high emphasis text such as input text in a
      * selected [TextField].
      */
-    @Composable
     val high: Float
+        @Composable
         get() = contentAlpha(
             highContrastAlpha = HighContrastContentAlpha.high,
             lowContrastAlpha = LowContrastContentAlpha.high
@@ -42,8 +42,8 @@
      * A medium level of content alpha, used to represent medium emphasis text such as
      * placeholder text in a [TextField].
      */
-    @Composable
     val medium: Float
+        @Composable
         get() = contentAlpha(
             highContrastAlpha = HighContrastContentAlpha.medium,
             lowContrastAlpha = LowContrastContentAlpha.medium
@@ -53,8 +53,8 @@
      * A low level of content alpha used to represent disabled components, such as text in a
      * disabled [Button].
      */
-    @Composable
     val disabled: Float
+        @Composable
         get() = contentAlpha(
             highContrastAlpha = HighContrastContentAlpha.disabled,
             lowContrastAlpha = LowContrastContentAlpha.disabled
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 5cb78fb..95656b7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -21,9 +21,9 @@
 import androidx.compose.animation.core.AnimationEndReason
 import androidx.compose.animation.core.SpringSpec
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
@@ -34,20 +34,23 @@
 import androidx.compose.runtime.savedinstancestate.Saver
 import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.WithConstraints
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.WithConstraints
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.semantics.dismiss
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.lerp
+import kotlin.math.roundToInt
 
 /**
  * Possible values of [DrawerState].
@@ -253,6 +256,8 @@
         )
     }
 
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
     companion object {
         /**
          * The default [Saver] implementation for [BottomDrawerState].
@@ -342,10 +347,10 @@
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     gesturesEnabled: Boolean = true,
     drawerShape: Shape = MaterialTheme.shapes.large,
-    drawerElevation: Dp = DrawerConstants.DefaultElevation,
+    drawerElevation: Dp = DrawerDefaults.Elevation,
     drawerBackgroundColor: Color = MaterialTheme.colors.surface,
     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
-    scrimColor: Color = DrawerConstants.defaultScrimColor,
+    scrimColor: Color = DrawerDefaults.scrimColor,
     bodyContent: @Composable () -> Unit
 ) {
     WithConstraints(modifier.fillMaxSize()) {
@@ -394,7 +399,7 @@
                             dismiss(action = { drawerState.close(); true })
                         }
                     }
-                    .offset(x = { drawerState.offset.value })
+                    .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
                     .padding(end = VerticalDrawerPadding),
                 shape = drawerShape,
                 color = drawerBackgroundColor,
@@ -445,10 +450,10 @@
     drawerState: BottomDrawerState = rememberBottomDrawerState(BottomDrawerValue.Closed),
     gesturesEnabled: Boolean = true,
     drawerShape: Shape = MaterialTheme.shapes.large,
-    drawerElevation: Dp = DrawerConstants.DefaultElevation,
+    drawerElevation: Dp = DrawerDefaults.Elevation,
     drawerBackgroundColor: Color = MaterialTheme.colors.surface,
     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
-    scrimColor: Color = DrawerConstants.defaultScrimColor,
+    scrimColor: Color = DrawerDefaults.scrimColor,
     bodyContent: @Composable () -> Unit
 ) {
     WithConstraints(modifier.fillMaxSize()) {
@@ -481,13 +486,15 @@
                 )
             }
         Box(
-            Modifier.swipeable(
-                state = drawerState,
-                anchors = anchors,
-                orientation = Orientation.Vertical,
-                enabled = gesturesEnabled,
-                resistance = null
-            )
+            Modifier
+                .nestedScroll(drawerState.nestedScrollConnection)
+                .swipeable(
+                    state = drawerState,
+                    anchors = anchors,
+                    orientation = Orientation.Vertical,
+                    enabled = gesturesEnabled,
+                    resistance = null
+                )
         ) {
             Box {
                 bodyContent()
@@ -514,8 +521,7 @@
                         if (drawerState.isOpen) {
                             dismiss(action = { drawerState.close(); true })
                         }
-                    }
-                    .offset(y = { drawerState.offset.value }),
+                    }.offset { IntOffset(0, drawerState.offset.value.roundToInt()) },
                 shape = drawerShape,
                 color = drawerBackgroundColor,
                 contentColor = drawerContentColor,
@@ -530,6 +536,13 @@
 /**
  * Object to hold default values for [ModalDrawerLayout] and [BottomDrawerLayout]
  */
+@Deprecated(
+    "DrawerConstants has been replaced with DrawerDefaults",
+    ReplaceWith(
+        "DrawerDefaults",
+        "androidx.compose.material.DrawerDefaults"
+    )
+)
 object DrawerConstants {
 
     /**
@@ -537,8 +550,8 @@
      */
     val DefaultElevation = 16.dp
 
-    @Composable
     val defaultScrimColor: Color
+        @Composable
         get() = MaterialTheme.colors.onSurface.copy(alpha = ScrimDefaultOpacity)
 
     /**
@@ -547,6 +560,26 @@
     const val ScrimDefaultOpacity = 0.32f
 }
 
+/**
+ * Object to hold default values for [ModalDrawerLayout] and [BottomDrawerLayout]
+ */
+object DrawerDefaults {
+
+    /**
+     * Default Elevation for drawer sheet as specified in material specs
+     */
+    val Elevation = 16.dp
+
+    val scrimColor: Color
+        @Composable
+        get() = MaterialTheme.colors.onSurface.copy(alpha = ScrimOpacity)
+
+    /**
+     * Default alpha for scrim color
+     */
+    const val ScrimOpacity = 0.32f
+}
+
 private fun calculateFraction(a: Float, b: Float, pos: Float) =
     ((pos - a) / (b - a)).coerceIn(0f, 1f)
 
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
index 781185d..daa9ef7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
@@ -29,8 +29,8 @@
 /**
  * Animates the [Dp] value of [this] between [from] and [to] [Interaction]s, to [target]. The
  * [AnimationSpec] used depends on the values for [from] and [to], see
- * [ElevationConstants.incomingAnimationSpecForInteraction] and
- * [ElevationConstants.outgoingAnimationSpecForInteraction] for more details.
+ * [ElevationDefaults.incomingAnimationSpecForInteraction] and
+ * [ElevationDefaults.outgoingAnimationSpecForInteraction] for more details.
  *
  * @param from the previous [Interaction] that was used to calculate elevation. `null` if there
  * was no previous [Interaction], such as when the component is in its default state.
@@ -47,9 +47,9 @@
 ) {
     val spec = when {
         // Moving to a new state
-        to != null -> ElevationConstants.incomingAnimationSpecForInteraction(to)
+        to != null -> ElevationDefaults.incomingAnimationSpecForInteraction(to)
         // Moving to default, from a previous state
-        from != null -> ElevationConstants.outgoingAnimationSpecForInteraction(from)
+        from != null -> ElevationDefaults.outgoingAnimationSpecForInteraction(from)
         // Loading the initial state, or moving back to the baseline state from a disabled /
         // unknown state, so just snap to the final value.
         else -> null
@@ -62,11 +62,18 @@
  *
  * Typically you should use [animateElevation] instead, which uses these [AnimationSpec]s
  * internally. [animateElevation] in turn is used by the defaults for [Button] and
- * [FloatingActionButton] - inside [ButtonConstants.defaultElevation] and
- * [FloatingActionButtonConstants.defaultElevation] respectively.
+ * [FloatingActionButton] - inside [ButtonDefaults.elevation] and
+ * [FloatingActionButtonDefaults.elevation] respectively.
  *
  * @see animateElevation
  */
+@Deprecated(
+    "ElevationConstants has been replaced with ElevationDefaults",
+    ReplaceWith(
+        "ElevationDefaults",
+        "androidx.compose.material.ElevationDefaults"
+    )
+)
 object ElevationConstants {
     /**
      * Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a
@@ -99,6 +106,48 @@
     }
 }
 
+/**
+ * Contains default [AnimationSpec]s used for animating elevation between different [Interaction]s.
+ *
+ * Typically you should use [animateElevation] instead, which uses these [AnimationSpec]s
+ * internally. [animateElevation] in turn is used by the defaults for [Button] and
+ * [FloatingActionButton] - inside [ButtonDefaults.elevation] and
+ * [FloatingActionButtonDefaults.elevation] respectively.
+ *
+ * @see animateElevation
+ */
+object ElevationDefaults {
+    /**
+     * Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a
+     * previous [Interaction], or from the default state. If [interaction] is unknown, then
+     * returns `null`.
+     *
+     * @param interaction the [Interaction] that is being animated to
+     */
+    fun incomingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
+        return when (interaction) {
+            is Interaction.Pressed -> DefaultIncomingSpec
+            is Interaction.Dragged -> DefaultIncomingSpec
+            else -> null
+        }
+    }
+
+    /**
+     * Returns the [AnimationSpec]s used when animating elevation away from [interaction], to the
+     * default state. If [interaction] is unknown, then returns `null`.
+     *
+     * @param interaction the [Interaction] that is being animated away from
+     */
+    fun outgoingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
+        return when (interaction) {
+            is Interaction.Pressed -> DefaultOutgoingSpec
+            is Interaction.Dragged -> DefaultOutgoingSpec
+            // TODO: use [HoveredOutgoingSpec] when hovered
+            else -> null
+        }
+    }
+}
+
 private val DefaultIncomingSpec = TweenSpec<Dp>(
     durationMillis = 120,
     easing = FastOutSlowInEasing
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
index 307565a..e92850d 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
@@ -80,18 +80,15 @@
     /**
      * Emphasis used to express high emphasis, such as for selected text fields.
      */
-    @Composable
-    val high: Emphasis
+    val high: Emphasis @Composable get
     /**
      * Emphasis used to express medium emphasis, such as for placeholder text in a text field.
      */
-    @Composable
-    val medium: Emphasis
+    val medium: Emphasis @Composable get
     /**
      * Emphasis used to express disabled state, such as for a disabled button.
      */
-    @Composable
-    val disabled: Emphasis
+    val disabled: Emphasis @Composable get
 }
 
 /**
@@ -148,24 +145,24 @@
         }
     }
 
-    @Composable
     override val high: Emphasis
+        @Composable
         get() = AlphaEmphasis(
             lightTheme = MaterialTheme.colors.isLight,
             highContrastAlpha = HighContrastAlphaLevels.high,
             reducedContrastAlpha = ReducedContrastAlphaLevels.high
         )
 
-    @Composable
     override val medium: Emphasis
+        @Composable
         get() = AlphaEmphasis(
             lightTheme = MaterialTheme.colors.isLight,
             highContrastAlpha = HighContrastAlphaLevels.medium,
             reducedContrastAlpha = ReducedContrastAlphaLevels.medium
         )
 
-    @Composable
     override val disabled: Emphasis
+        @Composable
         get() = AlphaEmphasis(
             lightTheme = MaterialTheme.colors.isLight,
             highContrastAlpha = HighContrastAlphaLevels.disabled,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
index ab7e7e6..40fcf2b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
@@ -17,10 +17,10 @@
 package androidx.compose.material
 
 import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.VectorConverter
 import androidx.compose.animation.asDisposableClock
 import androidx.compose.animation.core.AnimationClockObservable
 import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
 import androidx.compose.foundation.AmbientIndication
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
@@ -79,7 +79,7 @@
     shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
     backgroundColor: Color = MaterialTheme.colors.secondary,
     contentColor: Color = contentColorFor(backgroundColor),
-    elevation: FloatingActionButtonElevation = FloatingActionButtonConstants.defaultElevation(),
+    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
     content: @Composable () -> Unit
 ) {
     // TODO(aelias): Avoid manually managing the ripple once http://b/157687898
@@ -151,7 +151,7 @@
     shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
     backgroundColor: Color = MaterialTheme.colors.secondary,
     contentColor: Color = contentColorFor(backgroundColor),
-    elevation: FloatingActionButtonElevation = FloatingActionButtonConstants.defaultElevation()
+    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation()
 ) {
     FloatingActionButton(
         modifier = modifier.preferredSizeIn(
@@ -188,7 +188,7 @@
 /**
  * Represents the elevation for a floating action button in different states.
  *
- * See [FloatingActionButtonConstants.defaultElevation] for the default elevation used in a
+ * See [FloatingActionButtonDefaults.elevation] for the default elevation used in a
  * [FloatingActionButton] and [ExtendedFloatingActionButton].
  */
 @ExperimentalMaterialApi
@@ -205,6 +205,13 @@
 /**
  * Contains the default values used by [FloatingActionButton]
  */
+@Deprecated(
+    "FloatingActionButtonConstants has been replaced with FloatingActionButtonDefaults",
+    ReplaceWith(
+        "FloatingActionButtonDefaults",
+        "androidx.compose.material.FloatingActionButtonDefaults"
+    )
+)
 object FloatingActionButtonConstants {
     // TODO: b/152525426 add support for focused and hovered states
     /**
@@ -218,6 +225,15 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "FloatingActionButtonConstants has been replaced with " +
+            "FloatingActionButtonDefaults",
+        ReplaceWith(
+            "FloatingActionButtonDefaults.elevation(elevation, pressedElevation, " +
+                "disabledElevation)",
+            "androidx.compose.material.FloatingActionButtonDefaults"
+        )
+    )
     fun defaultElevation(
         defaultElevation: Dp = 6.dp,
         pressedElevation: Dp = 12.dp
@@ -236,6 +252,39 @@
 }
 
 /**
+ * Contains the default values used by [FloatingActionButton]
+ */
+object FloatingActionButtonDefaults {
+    // TODO: b/152525426 add support for focused and hovered states
+    /**
+     * Creates a [FloatingActionButtonElevation] that will animate between the provided values
+     * according to the Material specification.
+     *
+     * @param defaultElevation the elevation to use when the [FloatingActionButton] has no
+     * [Interaction]s
+     * @param pressedElevation the elevation to use when the [FloatingActionButton] is
+     * [Interaction.Pressed].
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun elevation(
+        defaultElevation: Dp = 6.dp,
+        pressedElevation: Dp = 12.dp
+        // focused: Dp = 8.dp,
+        // hovered: Dp = 8.dp,
+    ): FloatingActionButtonElevation {
+        val clock = AmbientAnimationClock.current.asDisposableClock()
+        return remember(defaultElevation, pressedElevation, clock) {
+            DefaultFloatingActionButtonElevation(
+                defaultElevation = defaultElevation,
+                pressedElevation = pressedElevation,
+                clock = clock
+            )
+        }
+    }
+}
+
+/**
  * Default [FloatingActionButtonElevation] implementation.
  */
 @OptIn(ExperimentalMaterialApi::class)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
index c79d3b5..c2b3ad0 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
@@ -22,7 +22,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.selection.toggleable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -68,7 +68,7 @@
                 onClick = onClick,
                 enabled = enabled,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(bounded = false, radius = RippleRadius)
+                indication = rememberRipple(bounded = false, radius = RippleRadius)
             )
             .then(IconButtonSizeModifier),
         contentAlignment = Alignment.Center
@@ -108,7 +108,7 @@
             onValueChange = onCheckedChange,
             enabled = enabled,
             interactionState = interactionState,
-            indication = rememberRippleIndication(bounded = false, radius = RippleRadius)
+            indication = rememberRipple(bounded = false, radius = RippleRadius)
         ).then(IconButtonSizeModifier),
         contentAlignment = Alignment.Center
     ) { content() }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
index 48e02b7..40b52dc 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
@@ -53,12 +53,15 @@
  * - three-line items
  * @sample androidx.compose.material.samples.ThreeLineListItems
  *
+ * You can combine this component with a checkbox or switch as in the following examples:
+ * @sample androidx.compose.material.samples.ClickableListItems
+ *
  * @param modifier Modifier to be applied to the list item
  * @param icon The leading supporting visual of the list item
  * @param secondaryText The secondary text of the list item
  * @param singleLineSecondaryText Whether the secondary text is single line
  * @param overlineText The text displayed above the primary text
- * @param trailing The trailing meta text or meta icon of the list item
+ * @param trailing The trailing meta text, icon, switch or checkbox
  * @param text The primary text of the list item
  */
 @Composable
@@ -108,20 +111,20 @@
 private object OneLine {
     // TODO(popam): support wide icons
     // TODO(popam): convert these to sp
-    // List item related constants.
+    // List item related defaults.
     private val MinHeight = 48.dp
     private val MinHeightWithIcon = 56.dp
 
-    // Icon related constants.
+    // Icon related defaults.
     private val IconMinPaddedWidth = 40.dp
     private val IconLeftPadding = 16.dp
     private val IconVerticalPadding = 8.dp
 
-    // Content related constants.
+    // Content related defaults.
     private val ContentLeftPadding = 16.dp
     private val ContentRightPadding = 16.dp
 
-    // Trailing related constants.
+    // Trailing related defaults.
     private val TrailingRightPadding = 16.dp
 
     @Composable
@@ -163,16 +166,16 @@
 }
 
 private object TwoLine {
-    // List item related constants.
+    // List item related defaults.
     private val MinHeight = 64.dp
     private val MinHeightWithIcon = 72.dp
 
-    // Icon related constants.
+    // Icon related defaults.
     private val IconMinPaddedWidth = 40.dp
     private val IconLeftPadding = 16.dp
     private val IconVerticalPadding = 16.dp
 
-    // Content related constants.
+    // Content related defaults.
     private val ContentLeftPadding = 16.dp
     private val ContentRightPadding = 16.dp
     private val OverlineBaselineOffset = 24.dp
@@ -182,7 +185,7 @@
     private val PrimaryToSecondaryBaselineOffsetNoIcon = 20.dp
     private val PrimaryToSecondaryBaselineOffsetWithIcon = 20.dp
 
-    // Trailing related constants.
+    // Trailing related defaults.
     private val TrailingRightPadding = 16.dp
 
     @Composable
@@ -264,15 +267,15 @@
 }
 
 private object ThreeLine {
-    // List item related constants.
+    // List item related defaults.
     private val MinHeight = 88.dp
 
-    // Icon related constants.
+    // Icon related defaults.
     private val IconMinPaddedWidth = 40.dp
     private val IconLeftPadding = 16.dp
     private val IconThreeLineVerticalPadding = 16.dp
 
-    // Content related constants.
+    // Content related defaults.
     private val ContentLeftPadding = 16.dp
     private val ContentRightPadding = 16.dp
     private val ThreeLineBaselineFirstOffset = 28.dp
@@ -280,7 +283,7 @@
     private val ThreeLineBaselineThirdOffset = 20.dp
     private val ThreeLineTrailingTopPadding = 16.dp
 
-    // Trailing related constants.
+    // Trailing related defaults.
     private val TrailingRightPadding = 16.dp
 
     @Composable
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
index 5cb947f..4cd516e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.graphics.useOrElse
 import androidx.compose.ui.selection.TextSelectionColors
 import androidx.compose.ui.util.annotation.VisibleForTesting
 import kotlin.math.max
@@ -39,9 +40,12 @@
     // Test with ContentAlpha.medium to ensure that the selection background is accessible in the
     // 'worst case' scenario. We explicitly don't test with ContentAlpha.disabled, as disabled
     // text shouldn't be selectable / is noted as disabled for accessibility purposes.
-    val textColorWithLowestAlpha = contentColorFor(backgroundColor).copy(
-        alpha = ContentAlpha.medium
-    )
+    val textColorWithLowestAlpha = colors.contentColorFor(backgroundColor)
+        .useOrElse {
+            AmbientContentColor.current
+        }.copy(
+            alpha = ContentAlpha.medium
+        )
     return remember(primaryColor, backgroundColor, textColorWithLowestAlpha) {
         TextSelectionColors(
             handleColor = colors.primary,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
index e65309a..8615164 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
@@ -18,9 +18,13 @@
 
 import androidx.compose.foundation.AmbientIndication
 import androidx.compose.foundation.Indication
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.AmbientRippleTheme
+import androidx.compose.material.ripple.ExperimentalRippleApi
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ComposableContract
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.remember
 import androidx.compose.ui.selection.AmbientTextSelectionColors
@@ -50,6 +54,7 @@
  * @param typography A set of text styles to be used as this hierarchy's typography system
  * @param shapes A set of shapes to be used by the components in this hierarchy
  */
+@OptIn(ExperimentalRippleApi::class)
 @Composable
 fun MaterialTheme(
     colors: Colors = MaterialTheme.colors,
@@ -63,15 +68,16 @@
         colors.copy()
     }.apply { updateColorsFrom(colors) }
     val indicationFactory: @Composable () -> Indication = remember {
-        { rememberRippleIndication() }
+        @Composable { rememberRipple() }
     }
     val selectionColors = rememberTextSelectionColors(rememberedColors)
     Providers(
         AmbientColors provides rememberedColors,
         AmbientContentAlpha provides ContentAlpha.high,
         AmbientIndication provides indicationFactory,
-        AmbientTextSelectionColors provides selectionColors,
+        AmbientRippleTheme provides MaterialRippleTheme,
         AmbientShapes provides shapes,
+        AmbientTextSelectionColors provides selectionColors,
         AmbientTypography provides typography
     ) {
         ProvideTextStyle(value = typography.body1, content = content)
@@ -88,9 +94,9 @@
      *
      * @sample androidx.compose.material.samples.ThemeColorSample
      */
-    @Composable
-    @ComposableContract(readonly = true)
     val colors: Colors
+        @Composable
+        @ComposableContract(readonly = true)
         get() = AmbientColors.current
 
     /**
@@ -98,16 +104,32 @@
      *
      * @sample androidx.compose.material.samples.ThemeTextStyleSample
      */
-    @Composable
-    @ComposableContract(readonly = true)
     val typography: Typography
+        @Composable
+        @ComposableContract(readonly = true)
         get() = AmbientTypography.current
 
     /**
      * Retrieves the current [Shapes] at the call site's position in the hierarchy.
      */
-    @Composable
-    @ComposableContract(readonly = true)
     val shapes: Shapes
+        @Composable
+        @ComposableContract(readonly = true)
         get() = AmbientShapes.current
 }
+
+@OptIn(ExperimentalRippleApi::class)
+@Immutable
+private object MaterialRippleTheme : RippleTheme {
+    @Composable
+    override fun defaultColor() = RippleTheme.defaultRippleColor(
+        contentColor = AmbientContentColor.current,
+        lightTheme = MaterialTheme.colors.isLight
+    )
+
+    @Composable
+    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
+        contentColor = AmbientContentColor.current,
+        lightTheme = MaterialTheme.colors.isLight
+    )
+}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index 2a09612..30cbd64 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -33,7 +33,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredSizeIn
 import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Providers
@@ -74,7 +74,8 @@
  * the [toggle], and then screen end-aligned. Vertically, it will try to expand to the bottom
  * of the [toggle], then from the top of the [toggle], and then screen top-aligned. A
  * [dropdownOffset] can be provided to adjust the positioning of the menu for cases when the
- * layout bounds of the [toggle] do not coincide with its visual bounds.
+ * layout bounds of the [toggle] do not coincide with its visual bounds. Note the offset will be
+ * applied in the direction in which the menu will decide to expand.
  *
  * Example usage:
  * @sample androidx.compose.material.samples.MenuSample
@@ -179,7 +180,7 @@
                 enabled = enabled,
                 onClick = onClick,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(true)
+                indication = rememberRipple(true)
             )
             .fillMaxWidth()
             // Preferred min and max width used during the intrinsic measurement.
@@ -199,7 +200,7 @@
     }
 }
 
-// Size constants.
+// Size defaults.
 private val MenuElevation = 8.dp
 private val MenuVerticalMargin = 32.dp
 private val DropdownMenuHorizontalPadding = 16.dp
@@ -305,8 +306,8 @@
         val contentOffsetY = with(density) { contentOffset.y.toIntPx() }
 
         // Compute horizontal position.
-        val toRight = parentGlobalBounds.right + contentOffsetX
-        val toLeft = parentGlobalBounds.left - contentOffsetX - popupContentSize.width
+        val toRight = parentGlobalBounds.left + contentOffsetX
+        val toLeft = parentGlobalBounds.right - contentOffsetX - popupContentSize.width
         val toDisplayRight = windowGlobalBounds.width - popupContentSize.width
         val toDisplayLeft = 0
         val x = if (layoutDirection == LayoutDirection.Ltr) {
@@ -318,7 +319,7 @@
         } ?: toLeft
 
         // Compute vertical position.
-        val toBottom = parentGlobalBounds.bottom + contentOffsetY
+        val toBottom = maxOf(parentGlobalBounds.bottom + contentOffsetY, verticalMargin)
         val toTop = parentGlobalBounds.top - contentOffsetY - popupContentSize.height
         val toCenter = parentGlobalBounds.top - popupContentSize.height / 2
         val toDisplayBottom = windowGlobalBounds.height - popupContentSize.height - verticalMargin
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 6e3b5198..08925b9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -23,9 +23,9 @@
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.offset
@@ -34,6 +34,7 @@
 import androidx.compose.runtime.savedinstancestate.Saver
 import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.graphics.Color
@@ -42,8 +43,10 @@
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import kotlin.math.max
+import kotlin.math.roundToInt
 
 /**
  * Possible values of [ModalBottomSheetState].
@@ -79,7 +82,7 @@
 class ModalBottomSheetState(
     initialValue: ModalBottomSheetValue,
     clock: AnimationClockObservable,
-    animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
     confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
 ) : SwipeableState<ModalBottomSheetValue>(
     initialValue = initialValue,
@@ -131,6 +134,8 @@
         )
     }
 
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
     companion object {
         /**
          * The default [Saver] implementation for [ModalBottomSheetState].
@@ -167,7 +172,7 @@
 fun rememberModalBottomSheetState(
     initialValue: ModalBottomSheetValue,
     clock: AnimationClockObservable = AmbientAnimationClock.current,
-    animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
     confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
 ): ModalBottomSheetState {
     val disposableClock = clock.asDisposableClock()
@@ -219,10 +224,10 @@
     sheetState: ModalBottomSheetState =
         rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
     sheetShape: Shape = MaterialTheme.shapes.large,
-    sheetElevation: Dp = ModalBottomSheetConstants.DefaultElevation,
+    sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
     sheetBackgroundColor: Color = MaterialTheme.colors.surface,
     sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
-    scrimColor: Color = ModalBottomSheetConstants.DefaultScrimColor,
+    scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
     content: @Composable () -> Unit
 ) = BottomSheetStack(
     modifier = modifier,
@@ -230,7 +235,8 @@
         Surface(
             Modifier
                 .fillMaxWidth()
-                .offset(y = { sheetState.offset.value }),
+                .nestedScroll(sheetState.nestedScrollConnection)
+                .offset { IntOffset(0, sheetState.offset.value.roundToInt()) },
             shape = sheetShape,
             elevation = sheetElevation,
             color = sheetBackgroundColor,
@@ -322,6 +328,13 @@
 /**
  * Contains useful constants for [ModalBottomSheetLayout].
  */
+@Deprecated(
+    "ModalBottomSheetConstants has been replaced with ModalBottomSheetDefaults",
+    ReplaceWith(
+        "ModalBottomSheetDefaults",
+        "androidx.compose.material.ModalBottomSheetDefaults"
+    )
+)
 object ModalBottomSheetConstants {
 
     /**
@@ -332,7 +345,25 @@
     /**
      * The default scrim color used by [ModalBottomSheetLayout].
      */
-    @Composable
     val DefaultScrimColor: Color
+        @Composable
+        get() = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
+}
+
+/**
+ * Contains useful Defaults for [ModalBottomSheetLayout].
+ */
+object ModalBottomSheetDefaults {
+
+    /**
+     * The default elevation used by [ModalBottomSheetLayout].
+     */
+    val Elevation = 16.dp
+
+    /**
+     * The default scrim color used by [ModalBottomSheetLayout].
+     */
+    val scrimColor: Color
+        @Composable
         get() = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
 }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index fa19662..653593a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -30,7 +30,6 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.RectangleShape
@@ -274,7 +273,6 @@
     )
 }
 
-@OptIn(ExperimentalFocus::class)
 @Composable
 internal fun OutlinedTextFieldLayout(
     modifier: Modifier = Modifier,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
index 09a97e0..096155b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
@@ -16,22 +16,21 @@
 
 package androidx.compose.material
 
-import androidx.compose.animation.core.AnimationConstants.Infinite
 import androidx.compose.animation.core.CubicBezierEasing
 import androidx.compose.animation.core.FloatPropKey
 import androidx.compose.animation.core.IntPropKey
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.repeatable
 import androidx.compose.animation.core.transitionDefinition
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.transition
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.progressSemantics
-import androidx.compose.material.ProgressIndicatorConstants.DefaultIndicatorBackgroundOpacity
+import androidx.compose.material.ProgressIndicatorDefaults.IndicatorBackgroundOpacity
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
@@ -53,7 +52,7 @@
  * A determinate linear progress indicator that represents progress by drawing a horizontal line.
  *
  * By default there is no animation between [progress] values. You can use
- * [ProgressIndicatorConstants.DefaultProgressAnimationSpec] as the default recommended
+ * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended
  * [AnimationSpec] when animating progress, such as in the following example:
  *
  * @sample androidx.compose.material.samples.LinearProgressIndicatorSample
@@ -69,14 +68,14 @@
     @FloatRange(from = 0.0, to = 1.0) progress: Float,
     modifier: Modifier = Modifier,
     color: Color = MaterialTheme.colors.primary,
-    backgroundColor: Color = color.copy(alpha = DefaultIndicatorBackgroundOpacity)
+    backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity)
 ) {
     Canvas(
         modifier
             .progressSemantics(progress)
             .preferredSize(LinearIndicatorWidth, LinearIndicatorHeight)
     ) {
-        val strokeWidth = ProgressIndicatorConstants.DefaultStrokeWidth.toPx()
+        val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
         drawLinearIndicatorBackground(backgroundColor, strokeWidth)
         drawLinearIndicator(0f, progress, color, strokeWidth)
     }
@@ -94,7 +93,7 @@
 fun LinearProgressIndicator(
     modifier: Modifier = Modifier,
     color: Color = MaterialTheme.colors.primary,
-    backgroundColor: Color = color.copy(alpha = DefaultIndicatorBackgroundOpacity)
+    backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity)
 ) {
     val state = transition(
         definition = LinearIndeterminateTransition,
@@ -110,7 +109,7 @@
         val firstLineTail = state[FirstLineTailProp]
         val secondLineHead = state[SecondLineHeadProp]
         val secondLineTail = state[SecondLineTailProp]
-        val strokeWidth = ProgressIndicatorConstants.DefaultStrokeWidth.toPx()
+        val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
         drawLinearIndicatorBackground(backgroundColor, strokeWidth)
         if (firstLineHead - firstLineTail > 0) {
             drawLinearIndicator(
@@ -160,7 +159,7 @@
  * 0 to 360 degrees.
  *
  * By default there is no animation between [progress] values. You can use
- * [ProgressIndicatorConstants.DefaultProgressAnimationSpec] as the default recommended
+ * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended
  * [AnimationSpec] when animating progress, such as in the following example:
  *
  * @sample androidx.compose.material.samples.CircularProgressIndicatorSample
@@ -175,7 +174,7 @@
     @FloatRange(from = 0.0, to = 1.0) progress: Float,
     modifier: Modifier = Modifier,
     color: Color = MaterialTheme.colors.primary,
-    strokeWidth: Dp = ProgressIndicatorConstants.DefaultStrokeWidth
+    strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth
 ) {
     val stroke = with(AmbientDensity.current) {
         Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Butt)
@@ -203,7 +202,7 @@
 fun CircularProgressIndicator(
     modifier: Modifier = Modifier,
     color: Color = MaterialTheme.colors.primary,
-    strokeWidth: Dp = ProgressIndicatorConstants.DefaultStrokeWidth
+    strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth
 ) {
     val stroke = with(AmbientDensity.current) {
         Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Square)
@@ -259,6 +258,13 @@
 /**
  * Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator].
  */
+@Deprecated(
+    "ProgressIndicatorConstants has been replaced with ProgressIndicatorDefaults",
+    ReplaceWith(
+        "ProgressIndicatorDefaults",
+        "androidx.compose.material.ProgressIndicatorDefaults"
+    )
+)
 object ProgressIndicatorConstants {
     /**
      * Default stroke width for [CircularProgressIndicator], and default height for
@@ -288,6 +294,38 @@
     )
 }
 
+/**
+ * Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator].
+ */
+object ProgressIndicatorDefaults {
+    /**
+     * Default stroke width for [CircularProgressIndicator], and default height for
+     * [LinearProgressIndicator].
+     *
+     * This can be customized with the `strokeWidth` parameter on [CircularProgressIndicator],
+     * and by passing a layout modifier setting the height for [LinearProgressIndicator].
+     */
+    val StrokeWidth = 4.dp
+
+    /**
+     * The default opacity applied to the indicator color to create the background color in a
+     * [LinearProgressIndicator].
+     */
+    const val IndicatorBackgroundOpacity = 0.24f
+
+    /**
+     * The default [AnimationSpec] that should be used when animating between progress in a
+     * determinate progress indicator.
+     */
+    val ProgressAnimationSpec = SpringSpec(
+        dampingRatio = Spring.DampingRatioNoBouncy,
+        stiffness = Spring.StiffnessVeryLow,
+        // The default threshold is 0.01, or 1% of the overall progress range, which is quite
+        // large and noticeable.
+        visibilityThreshold = 1 / 1000f
+    )
+}
+
 private fun DrawScope.drawDeterminateCircularIndicator(
     startAngle: Float,
     sweep: Float,
@@ -322,7 +360,7 @@
 // LinearProgressIndicator Material specs
 // TODO: there are currently 3 fixed widths in Android, should this be flexible? Material says
 // the width should be 240dp here.
-private val LinearIndicatorHeight = ProgressIndicatorConstants.DefaultStrokeWidth
+private val LinearIndicatorHeight = ProgressIndicatorDefaults.StrokeWidth
 private val LinearIndicatorWidth = 240.dp
 
 // CircularProgressIndicator Material specs
@@ -373,32 +411,28 @@
     }
 
     transition(fromState = 0, toState = 1) {
-        FirstLineHeadProp using repeatable(
-            iterations = Infinite,
+        FirstLineHeadProp using infiniteRepeatable(
             animation = keyframes {
                 durationMillis = LinearAnimationDuration
                 0f at FirstLineHeadDelay with FirstLineHeadEasing
                 1f at FirstLineHeadDuration + FirstLineHeadDelay
             }
         )
-        FirstLineTailProp using repeatable(
-            iterations = Infinite,
+        FirstLineTailProp using infiniteRepeatable(
             animation = keyframes {
                 durationMillis = LinearAnimationDuration
                 0f at FirstLineTailDelay with FirstLineTailEasing
                 1f at FirstLineTailDuration + FirstLineTailDelay
             }
         )
-        SecondLineHeadProp using repeatable(
-            iterations = Infinite,
+        SecondLineHeadProp using infiniteRepeatable(
             animation = keyframes {
                 durationMillis = LinearAnimationDuration
                 0f at SecondLineHeadDelay with SecondLineHeadEasing
                 1f at SecondLineHeadDuration + SecondLineHeadDelay
             }
         )
-        SecondLineTailProp using repeatable(
-            iterations = Infinite,
+        SecondLineTailProp using infiniteRepeatable(
             animation = keyframes {
                 durationMillis = LinearAnimationDuration
                 0f at SecondLineTailDelay with SecondLineTailEasing
@@ -462,30 +496,26 @@
     }
 
     transition(fromState = 0, toState = 1) {
-        IterationProp using repeatable(
-            iterations = Infinite,
+        IterationProp using infiniteRepeatable(
             animation = tween(
                 durationMillis = RotationDuration * RotationsPerCycle,
                 easing = LinearEasing
             )
         )
-        BaseRotationProp using repeatable(
-            iterations = Infinite,
+        BaseRotationProp using infiniteRepeatable(
             animation = tween(
                 durationMillis = RotationDuration,
                 easing = LinearEasing
             )
         )
-        HeadRotationProp using repeatable(
-            iterations = Infinite,
+        HeadRotationProp using infiniteRepeatable(
             animation = keyframes {
                 durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration
                 0f at 0 with CircularEasing
                 JumpRotationAngle at HeadAndTailAnimationDuration
             }
         )
-        TailRotationProp using repeatable(
-            iterations = Infinite,
+        TailRotationProp using infiniteRepeatable(
             animation = keyframes {
                 durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration
                 0f at HeadAndTailDelayDuration with CircularEasing
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
index 8a8a6eb..61ba138 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
@@ -32,7 +32,7 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
@@ -66,7 +66,7 @@
  * [InteractionState] if you want to read the [InteractionState] and customize the appearance /
  * behavior of this RadioButton in different [Interaction]s.
  * @param colors [RadioButtonColors] that will be used to resolve the color used for this
- * RadioButton in different states. See [RadioButtonConstants.defaultColors].
+ * RadioButton in different states. See [RadioButtonDefaults.colors].
  */
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
@@ -76,7 +76,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionState: InteractionState = remember { InteractionState() },
-    colors: RadioButtonColors = RadioButtonConstants.defaultColors()
+    colors: RadioButtonColors = RadioButtonDefaults.colors()
 ) {
     val dotRadius = animate(
         target = if (selected) RadioButtonDotSize / 2 else 0.dp,
@@ -89,7 +89,7 @@
                 onClick = onClick,
                 enabled = enabled,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(
+                indication = rememberRipple(
                     bounded = false,
                     radius = RadioButtonRippleRadius
                 )
@@ -106,7 +106,7 @@
 /**
  * Represents the color used by a [RadioButton] in different states.
  *
- * See [RadioButtonConstants.defaultColors] for the default implementation that follows Material
+ * See [RadioButtonDefaults.colors] for the default implementation that follows Material
  * specifications.
  */
 @ExperimentalMaterialApi
@@ -125,6 +125,13 @@
 /**
  * Constants used in [RadioButton].
  */
+@Deprecated(
+    "RadioButtonConstants has been replaced with RadioButtonDefaults",
+    ReplaceWith(
+        "RadioButtonDefaults",
+        "androidx.compose.material.RadioButtonDefaults"
+    )
+)
 object RadioButtonConstants {
     /**
      * Creates a [RadioButtonColors] that will animate between the provided colors according to
@@ -137,6 +144,13 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "RadioButtonConstants has been replaced with RadioButtonDefaults",
+        ReplaceWith(
+            "RadioButtonDefaults.colors(selectedColor, unselectedColor, disabledColor)",
+            "androidx.compose.material.RadioButtonDefaults"
+        )
+    )
     fun defaultColors(
         selectedColor: Color = MaterialTheme.colors.secondary,
         unselectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
@@ -154,6 +168,38 @@
     }
 }
 
+/**
+ * Defaults used in [RadioButton].
+ */
+object RadioButtonDefaults {
+    /**
+     * Creates a [RadioButtonColors] that will animate between the provided colors according to
+     * the Material specification.
+     *
+     * @param selectedColor the color to use for the RadioButton when selected and enabled.
+     * @param unselectedColor the color to use for the RadioButton when unselected and enabled.
+     * @param disabledColor the color to use for the RadioButton when disabled.
+     * @return the resulting [Color] used for the RadioButton
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun colors(
+        selectedColor: Color = MaterialTheme.colors.secondary,
+        unselectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
+        disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
+    ): RadioButtonColors {
+        val clock = AmbientAnimationClock.current.asDisposableClock()
+        return remember(
+            selectedColor,
+            unselectedColor,
+            disabledColor,
+            clock
+        ) {
+            DefaultRadioButtonColors(selectedColor, unselectedColor, disabledColor, clock)
+        }
+    }
+}
+
 private fun DrawScope.drawRadio(color: Color, dotRadius: Dp) {
     val strokeWidth = RadioStrokeWidth.toPx()
     drawCircle(color, RadioRadius.toPx() - strokeWidth / 2, style = Stroke(strokeWidth))
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 33f7dd15..00f6b24 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -160,10 +160,10 @@
     drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
     drawerGesturesEnabled: Boolean = true,
     drawerShape: Shape = MaterialTheme.shapes.large,
-    drawerElevation: Dp = DrawerConstants.DefaultElevation,
+    drawerElevation: Dp = DrawerDefaults.Elevation,
     drawerBackgroundColor: Color = MaterialTheme.colors.surface,
     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
-    drawerScrimColor: Color = DrawerConstants.defaultScrimColor,
+    drawerScrimColor: Color = DrawerDefaults.scrimColor,
     backgroundColor: Color = MaterialTheme.colors.background,
     contentColor: Color = contentColorFor(backgroundColor),
     bodyContent: @Composable (PaddingValues) -> Unit
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index 05b57ec..c6b155b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -29,6 +29,7 @@
 import androidx.compose.foundation.animation.FlingConfig
 import androidx.compose.foundation.animation.defaultFlingConfig
 import androidx.compose.foundation.animation.fling
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.indication
 import androidx.compose.foundation.layout.Box
@@ -39,9 +40,9 @@
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.layout.preferredWidthIn
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.SliderConstants.InactiveTrackColorAlpha
-import androidx.compose.material.SliderConstants.TickColorAlpha
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.SliderDefaults.InactiveTrackColorAlpha
+import androidx.compose.material.SliderDefaults.TickColorAlpha
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -58,8 +59,8 @@
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.semantics.AccessibilityRangeInfo
-import androidx.compose.ui.semantics.accessibilityValue
-import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.stateDescriptionRange
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.setProgress
 import androidx.compose.ui.unit.LayoutDirection
@@ -195,6 +196,13 @@
 /**
  * Object to hold constants used by the [Slider]
  */
+@Deprecated(
+    "SliderConstants has been replaced with SliderDefaults",
+    ReplaceWith(
+        "SliderDefaults",
+        "androidx.compose.material.SliderDefaults"
+    )
+)
 object SliderConstants {
     /**
      * Default alpha of the inactive part of the track
@@ -207,6 +215,21 @@
     const val TickColorAlpha = 0.54f
 }
 
+/**
+ * Object to hold defaults used by [Slider]
+ */
+object SliderDefaults {
+    /**
+     * Default alpha of the inactive part of the track
+     */
+    const val InactiveTrackColorAlpha = 0.24f
+
+    /**
+     * Default alpha of the ticks that are drawn on top of the track
+     */
+    const val TickColorAlpha = 0.54f
+}
+
 @Composable
 private fun SliderImpl(
     positionFraction: Float,
@@ -257,13 +280,15 @@
                 shape = CircleShape,
                 color = thumbColor,
                 elevation = elevation,
-                modifier = Modifier.indication(
-                    interactionState = interactionState,
-                    indication = rememberRippleIndication(
-                        bounded = false,
-                        radius = ThumbRippleRadius
+                modifier = Modifier
+                    .focusable(interactionState = interactionState)
+                    .indication(
+                        interactionState = interactionState,
+                        indication = rememberRipple(
+                            bounded = false,
+                            radius = ThumbRippleRadius
+                        )
                     )
-                )
             ) {
                 Spacer(Modifier.preferredSize(thumbSize, thumbSize))
             }
@@ -357,9 +382,9 @@
         1f -> 100
         else -> (fraction * 100).roundToInt().coerceIn(1, 99)
     }
-    return semantics {
-        accessibilityValue = Strings.TemplatePercent.format(percent)
-        accessibilityValueRange = AccessibilityRangeInfo(coerced, valueRange, steps)
+    return semantics(mergeDescendants = true) {
+        stateDescription = Strings.TemplatePercent.format(percent)
+        stateDescriptionRange = AccessibilityRangeInfo(coerced, valueRange, steps)
         setProgress(
             action = { targetValue ->
                 val newValue = targetValue.coerceIn(position.startValue, position.endValue)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
index 41a9958..dcdafed 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
@@ -59,7 +59,7 @@
  *
  * @param modifier modifiers for the the Snackbar layout
  * @param action action / button component to add as an action to the snackbar. Consider using
- * [SnackbarConstants.defaultActionPrimaryColor] as the color for the action, if you do not
+ * [SnackbarDefaults.primaryActionColor] as the color for the action, if you do not
  * have a predefined color you wish to use instead.
  * @param actionOnNewLine whether or not action should be put on the separate line. Recommended
  * for action with long action text
@@ -79,7 +79,7 @@
     action: @Composable (() -> Unit)? = null,
     actionOnNewLine: Boolean = false,
     shape: Shape = MaterialTheme.shapes.small,
-    backgroundColor: Color = SnackbarConstants.defaultBackgroundColor,
+    backgroundColor: Color = SnackbarDefaults.backgroundColor,
     contentColor: Color = MaterialTheme.colors.surface,
     elevation: Dp = 6.dp,
     text: @Composable () -> Unit
@@ -147,16 +147,16 @@
     modifier: Modifier = Modifier,
     actionOnNewLine: Boolean = false,
     shape: Shape = MaterialTheme.shapes.small,
-    backgroundColor: Color = SnackbarConstants.defaultBackgroundColor,
+    backgroundColor: Color = SnackbarDefaults.backgroundColor,
     contentColor: Color = MaterialTheme.colors.surface,
-    actionColor: Color = SnackbarConstants.defaultActionPrimaryColor,
+    actionColor: Color = SnackbarDefaults.primaryActionColor,
     elevation: Dp = 6.dp
 ) {
     val actionLabel = snackbarData.actionLabel
     val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
         {
             TextButton(
-                colors = ButtonConstants.defaultTextButtonColors(contentColor = actionColor),
+                colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
                 onClick = { snackbarData.performAction() },
                 content = { Text(actionLabel) }
             )
@@ -179,6 +179,13 @@
 /**
  * Object to hold constants used by the [Snackbar]
  */
+@Deprecated(
+    "SnackbarConstants has been replaced with SnackbarDefaults",
+    ReplaceWith(
+        "SnackbarDefaults",
+        "androidx.compose.material.SnackbarDefaults"
+    )
+)
 object SnackbarConstants {
 
     /**
@@ -189,8 +196,8 @@
     /**
      * Default background color of the [Snackbar]
      */
-    @Composable
     val defaultBackgroundColor: Color
+        @Composable
         get() =
             MaterialTheme.colors.onSurface
                 .copy(alpha = SnackbarOverlayAlpha)
@@ -210,8 +217,57 @@
      * [MaterialTheme.colors] to attempt to reduce the contrast, and when in a dark theme this
      * function uses [Colors.primaryVariant].
      */
-    @Composable
     val defaultActionPrimaryColor: Color
+        @Composable
+        get() {
+            val colors = MaterialTheme.colors
+            return if (colors.isLight) {
+                val primary = colors.primary
+                val overlayColor = colors.surface.copy(alpha = 0.6f)
+
+                overlayColor.compositeOver(primary)
+            } else {
+                colors.primaryVariant
+            }
+        }
+}
+
+/**
+ * Object to hold defaults used by [Snackbar]
+ */
+object SnackbarDefaults {
+
+    /**
+     * Default alpha of the overlay applied to the [backgroundColor]
+     */
+    private const val SnackbarOverlayAlpha = 0.8f
+
+    /**
+     * Default background color of the [Snackbar]
+     */
+    val backgroundColor: Color
+        @Composable
+        get() =
+            MaterialTheme.colors.onSurface
+                .copy(alpha = SnackbarOverlayAlpha)
+                .compositeOver(MaterialTheme.colors.surface)
+
+    /**
+     * Provides a best-effort 'primary' color to be used as the primary color inside a [Snackbar].
+     * Given that [Snackbar]s have an 'inverted' theme, i.e in a light theme they appear dark, and
+     * in a dark theme they appear light, just using [Colors.primary] will not work, and has
+     * incorrect contrast.
+     *
+     * If your light theme has a corresponding dark theme, you should instead directly use
+     * [Colors.primary] from the dark theme when in a light theme, and use
+     * [Colors.primaryVariant] from the dark theme when in a dark theme.
+     *
+     * When in a light theme, this function applies a color overlay to [Colors.primary] from
+     * [MaterialTheme.colors] to attempt to reduce the contrast, and when in a dark theme this
+     * function uses [Colors.primaryVariant].
+     */
+    val primaryActionColor: Color
+        @Composable
         get() {
             val colors = MaterialTheme.colors
             return if (colors.isLight) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
index b0b7b05..2adefd7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
@@ -28,8 +28,8 @@
 import androidx.compose.material.DismissValue.Default
 import androidx.compose.material.DismissValue.DismissedToEnd
 import androidx.compose.material.DismissValue.DismissedToStart
-import androidx.compose.material.SwipeableConstants.StandardResistanceFactor
-import androidx.compose.material.SwipeableConstants.StiffResistanceFactor
+import androidx.compose.material.SwipeableDefaults.StandardResistanceFactor
+import androidx.compose.material.SwipeableDefaults.StiffResistanceFactor
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.savedinstancestate.Saver
@@ -39,7 +39,9 @@
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.roundToInt
 
 /**
  * The directions in which a [SwipeToDismiss] can be dismissed.
@@ -239,7 +241,7 @@
         )
         Row(
             content = dismissContent,
-            modifier = Modifier.offset(x = { state.offset.value })
+            modifier = Modifier.offset { IntOffset(state.offset.value.roundToInt(), 0) }
         )
     }
 }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
index 858066e..2bcbfb2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
@@ -25,10 +25,10 @@
 import androidx.compose.animation.core.SpringSpec
 import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.gestures.draggable
-import androidx.compose.material.SwipeableConstants.DefaultAnimationSpec
-import androidx.compose.material.SwipeableConstants.DefaultVelocityThreshold
-import androidx.compose.material.SwipeableConstants.StandardResistanceFactor
-import androidx.compose.material.SwipeableConstants.defaultResistanceConfig
+import androidx.compose.material.SwipeableDefaults.AnimationSpec
+import androidx.compose.material.SwipeableDefaults.VelocityThreshold
+import androidx.compose.material.SwipeableDefaults.StandardResistanceFactor
+import androidx.compose.material.SwipeableDefaults.resistanceConfig
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
@@ -42,12 +42,16 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.annotation.FloatRange
 import androidx.compose.ui.util.lerp
@@ -73,7 +77,7 @@
 open class SwipeableState<T>(
     initialValue: T,
     clock: AnimationClockObservable,
-    internal val animationSpec: AnimationSpec<Float> = DefaultAnimationSpec,
+    internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
     internal val confirmStateChange: (newValue: T) -> Boolean = { true }
 ) {
     /**
@@ -109,8 +113,8 @@
      */
     val overflow: State<Float> get() = overflowState
 
-    private var minBound = Float.NEGATIVE_INFINITY
-    private var maxBound = Float.POSITIVE_INFINITY
+    internal var minBound = Float.NEGATIVE_INFINITY
+    internal var maxBound = Float.POSITIVE_INFINITY
 
     private val anchorsState = mutableStateOf(emptyMap<Float, T>())
 
@@ -167,6 +171,8 @@
 
     internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
 
+    internal var velocityThreshold by mutableStateOf(0f)
+
     internal var resistance: ResistanceConfig? by mutableStateOf(null)
 
     internal val holder: AnimatedFloat = NotificationBasedAnimatedFloat(0f, animationClockProxy) {
@@ -290,6 +296,63 @@
         }
     }
 
+    /**
+     * Perform fling with settling to one of the anchors which is determined by the given
+     * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
+     * since it will settle at the anchor.
+     *
+     * In general cases, [swipeable] flings by itself when being swiped. This method is to be
+     * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+     * want to trigger settling fling when the child scroll container reaches the bound.
+     *
+     * @param velocity velocity to fling and settle with
+     * @param onEnd callback to be invoked when fling is completed
+     */
+    fun performFling(velocity: Float, onEnd: (() -> Unit)) {
+        val lastAnchor = anchors.getOffset(value)!!
+        val targetValue = computeTarget(
+            offset = offset.value,
+            lastValue = lastAnchor,
+            anchors = anchors.keys,
+            thresholds = thresholds,
+            velocity = velocity,
+            velocityThreshold = velocityThreshold
+        )
+        val targetState = anchors[targetValue]
+        if (targetState != null && confirmStateChange(targetState)) {
+            animateTo(targetState, onEnd = { _, _ -> onEnd() })
+        } else {
+            // If the user vetoed the state change, rollback to the previous state.
+            holder.animateTo(lastAnchor, animationSpec, onEnd = { _, _ -> onEnd() })
+        }
+    }
+
+    /**
+     * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
+     * gesture flow.
+     *
+     * Note: This method performs generic drag and it won't settle to any particular anchor, *
+     * leaving swipeable in between anchors. When done dragging, [performFling] must be
+     * called as well to ensure swipeable will settle at the anchor.
+     *
+     * In general cases, [swipeable] drags by itself when being swiped. This method is to be
+     * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+     * want to force drag when the child scroll container reaches the bound.
+     *
+     * @param delta delta in pixels to drag by
+     *
+     * @return the amount of [delta] consumed
+     */
+    fun performDrag(delta: Float): Float {
+        val potentiallyConsumed = holder.value + delta
+        val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
+        val deltaToConsume = clamped - holder.value
+        if (abs(deltaToConsume) > 0) {
+            holder.snapTo(holder.value + deltaToConsume)
+        }
+        return deltaToConsume
+    }
+
     companion object {
         /**
          * The default [Saver] implementation for [SwipeableState].
@@ -333,7 +396,7 @@
 @ExperimentalMaterialApi
 fun <T : Any> rememberSwipeableState(
     initialValue: T,
-    animationSpec: AnimationSpec<Float> = DefaultAnimationSpec,
+    animationSpec: AnimationSpec<Float> = AnimationSpec,
     confirmStateChange: (newValue: T) -> Boolean = { true }
 ): SwipeableState<T> {
     val clock = AmbientAnimationClock.current.asDisposableClock()
@@ -367,7 +430,7 @@
 internal fun <T : Any> rememberSwipeableStateFor(
     value: T,
     onValueChange: (T) -> Unit,
-    animationSpec: AnimationSpec<Float> = DefaultAnimationSpec
+    animationSpec: AnimationSpec<Float> = AnimationSpec
 ): SwipeableState<T> {
     val swipeableState = rememberSwipeableState(
         initialValue = value,
@@ -433,8 +496,8 @@
     reverseDirection: Boolean = false,
     interactionState: InteractionState? = null,
     thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
-    resistance: ResistanceConfig? = defaultResistanceConfig(anchors.keys),
-    velocityThreshold: Dp = DefaultVelocityThreshold
+    resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
+    velocityThreshold: Dp = VelocityThreshold
 ) = composed(
     inspectorInfo = debugInspectorInfo {
         name = "swipeable"
@@ -463,6 +526,9 @@
             val to = anchors.getValue(b)
             with(thresholds(from, to)) { density.computeThreshold(a, b) }
         }
+        with(density) {
+            state.velocityThreshold = velocityThreshold.toPx()
+        }
     }
     onCommit {
         state.resistance = resistance
@@ -475,22 +541,7 @@
         interactionState = interactionState,
         startDragImmediately = state.isAnimationRunning,
         onDragStopped = { velocity ->
-            val lastAnchor = anchors.getOffset(state.value)!!
-            val targetValue = computeTarget(
-                offset = state.offset.value,
-                lastValue = lastAnchor,
-                anchors = anchors.keys,
-                thresholds = state.thresholds,
-                velocity = velocity,
-                velocityThreshold = with(density) { velocityThreshold.toPx() }
-            )
-            val targetState = anchors[targetValue]
-            if (targetState != null && state.confirmStateChange(targetState)) {
-                state.animateTo(targetState)
-            } else {
-                // If the user vetoed the state change, rollback to the previous state.
-                state.holder.animateTo(lastAnchor, state.animationSpec)
-            }
+            state.performFling(velocity) {}
         }
     ) { delta ->
         state.holder.snapTo(state.holder.value + delta)
@@ -548,10 +599,10 @@
  *
  * The resistance basis is usually either the size of the component which [swipeable] is applied
  * to, or the distance between the minimum and maximum anchors. For a constructor in which the
- * resistance basis defaults to the latter, consider using [defaultResistanceConfig].
+ * resistance basis defaults to the latter, consider using [resistanceConfig].
  *
  * You may specify different resistance factors for each bound. Consider using one of the default
- * resistance factors in [SwipeableConstants]: `StandardResistanceFactor` to convey that the user
+ * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user
  * has run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe
  * this right now. Also, you can set either factor to 0 to disable resistance at that bound.
  *
@@ -610,9 +661,9 @@
     offset: Float,
     anchors: Set<Float>
 ): List<Float> {
-    // Find the anchors the target lies between.
-    val a = anchors.filter { it <= offset }.maxOrNull()
-    val b = anchors.filter { it >= offset }.minOrNull()
+    // Find the anchors the target lies between with a little bit of rounding error.
+    val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
+    val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
 
     return when {
         a == null ->
@@ -673,6 +724,13 @@
 /**
  * Contains useful constants for [swipeable] and [SwipeableState].
  */
+@Deprecated(
+    "SwipeableConstants has been replaced with SwipeableDefaults",
+    ReplaceWith(
+        "SwipeableDefaults",
+        "androidx.compose.material.SwipeableDefaults"
+    )
+)
 object SwipeableConstants {
     /**
      * The default animation used by [SwipeableState].
@@ -712,4 +770,101 @@
             ResistanceConfig(basis, factorAtMin, factorAtMax)
         }
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Contains useful defaults for [swipeable] and [SwipeableState].
+ */
+object SwipeableDefaults {
+    /**
+     * The default animation used by [SwipeableState].
+     */
+    val AnimationSpec = SpringSpec<Float>()
+
+    /**
+     * The default velocity threshold (1.8 dp per millisecond) used by [swipeable].
+     */
+    val VelocityThreshold = 125.dp
+
+    /**
+     * A stiff resistance factor which indicates that swiping isn't available right now.
+     */
+    const val StiffResistanceFactor = 20f
+
+    /**
+     * A standard resistance factor which indicates that the user has run out of things to see.
+     */
+    const val StandardResistanceFactor = 10f
+
+    /**
+     * The default resistance config used by [swipeable].
+     *
+     * This returns `null` if there is one anchor. If there are at least two anchors, it returns
+     * a [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
+     */
+    fun resistanceConfig(
+        anchors: Set<Float>,
+        factorAtMin: Float = StandardResistanceFactor,
+        factorAtMax: Float = StandardResistanceFactor
+    ): ResistanceConfig? {
+        return if (anchors.size <= 1) {
+            null
+        } else {
+            val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
+            ResistanceConfig(basis, factorAtMin, factorAtMax)
+        }
+    }
+}
+
+// temp default nested scroll connection for swipeables which desire as an opt in
+// revisit in b/174756744 as all types will have their own specific connection probably
+@ExperimentalMaterialApi
+internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
+    get() = object : NestedScrollConnection {
+        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+            val delta = available.toFloat()
+            return if (delta < 0 && source == NestedScrollSource.Drag) {
+                performDrag(delta).toOffset()
+            } else {
+                Offset.Zero
+            }
+        }
+
+        override fun onPostScroll(
+            consumed: Offset,
+            available: Offset,
+            source: NestedScrollSource
+        ): Offset {
+            return if (source == NestedScrollSource.Drag) {
+                performDrag(available.toFloat()).toOffset()
+            } else {
+                Offset.Zero
+            }
+        }
+
+        override fun onPreFling(available: Velocity): Velocity {
+            val toFling = available.pixelsPerSecond.toFloat()
+            return if (toFling < 0 && offset.value > minBound) {
+                performFling(velocity = toFling) {}
+                // since we go to the anchor with tween settling, consume all for the best UX
+                available
+            } else {
+                Velocity.Zero
+            }
+        }
+
+        override fun onPostFling(
+            consumed: Velocity,
+            available: Velocity,
+            onFinished: (Velocity) -> Unit
+        ) {
+            performFling(velocity = available.pixelsPerSecond.toFloat()) {
+                // since we go to the anchor with tween settling, consume all for the best UX
+                onFinished.invoke(available)
+            }
+        }
+
+        private fun Float.toOffset(): Offset = Offset(0f, this)
+
+        private fun Offset.toFloat(): Float = this.y
+    }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
index 4ad2c29..fc9dbcd 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
@@ -30,7 +30,7 @@
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.toggleable
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
@@ -47,8 +47,10 @@
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
 
 /**
  * A Switch is a two state toggleable component that provides on/off like options
@@ -65,7 +67,7 @@
  * [InteractionState] if you want to read the [InteractionState] and customize the appearance /
  * behavior of this Switch in different [Interaction]s.
  * @param colors [SwitchColors] that will be used to determine the color of the thumb and track
- * in different states. See [SwitchConstants.defaultColors].
+ * in different states. See [SwitchDefaults.colors].
  */
 @Composable
 @OptIn(ExperimentalMaterialApi::class)
@@ -75,7 +77,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionState: InteractionState = remember { InteractionState() },
-    colors: SwitchColors = SwitchConstants.defaultColors()
+    colors: SwitchColors = SwitchDefaults.colors()
 ) {
     val minBound = 0f
     val maxBound = with(AmbientDensity.current) { ThumbPathLength.toPx() }
@@ -117,7 +119,7 @@
 /**
  * Represents the colors used by a [Switch] in different states
  *
- * See [SwitchConstants.defaultColors] for the default implementation that follows Material
+ * See [SwitchDefaults.colors] for the default implementation that follows Material
  * specifications.
  */
 @ExperimentalMaterialApi
@@ -168,10 +170,10 @@
         elevation = elevation,
         modifier = Modifier
             .align(Alignment.CenterStart)
-            .offset(x = { thumbValue.value })
+            .offset { IntOffset(thumbValue.value.roundToInt(), 0) }
             .indication(
                 interactionState = interactionState,
-                indication = rememberRippleIndication(bounded = false, radius = ThumbRippleRadius)
+                indication = rememberRipple(bounded = false, radius = ThumbRippleRadius)
             )
             .size(ThumbDiameter),
         content = emptyContent()
@@ -208,6 +210,13 @@
 /**
  * Contains the default values used by [Switch]
  */
+@Deprecated(
+    "SwitchConstants has been replaced with SwitchDefaults",
+    ReplaceWith(
+        "SwitchDefaults",
+        "androidx.compose.material.SwitchDefaults"
+    )
+)
 object SwitchConstants {
     /**
      * Creates a [SwitchColors] that represents the different colors used in a [Switch] in
@@ -228,6 +237,16 @@
      */
     @OptIn(ExperimentalMaterialApi::class)
     @Composable
+    @Deprecated(
+        "SwitchConstants has been replaced with SwitchDefaults",
+        ReplaceWith(
+            "SwitchDefaults.colors(checkedThumbColor, checkedTrackColor, checkedTrackAlpha, " +
+                "uncheckedThumbColor, uncheckedTrackColor, uncheckedTrackAlpha, " +
+                "disabledCheckedThumbColor, disabledCheckedTrackColor, " +
+                "disabledUncheckedThumbColor, disabledUncheckedTrackColor)",
+            "androidx.compose.material.SwitchDefaults"
+        )
+    )
     fun defaultColors(
         checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
         checkedTrackColor: Color = checkedThumbColor,
@@ -260,6 +279,60 @@
 }
 
 /**
+ * Contains the default values used by [Switch]
+ */
+object SwitchDefaults {
+    /**
+     * Creates a [SwitchColors] that represents the different colors used in a [Switch] in
+     * different states.
+     *
+     * @param checkedThumbColor the color used for the thumb when enabled and checked
+     * @param checkedTrackColor the color used for the track when enabled and checked
+     * @param checkedTrackAlpha the alpha applied to [checkedTrackColor] and
+     * [disabledCheckedTrackColor]
+     * @param uncheckedThumbColor the color used for the thumb when enabled and unchecked
+     * @param uncheckedTrackColor the color used for the track when enabled and unchecked
+     * @param uncheckedTrackAlpha the alpha applied to [uncheckedTrackColor] and
+     * [disabledUncheckedTrackColor]
+     * @param disabledCheckedThumbColor the color used for the thumb when disabled and checked
+     * @param disabledCheckedTrackColor the color used for the track when disabled and checked
+     * @param disabledUncheckedThumbColor the color used for the thumb when disabled and unchecked
+     * @param disabledUncheckedTrackColor the color used for the track when disabled and unchecked
+     */
+    @OptIn(ExperimentalMaterialApi::class)
+    @Composable
+    fun colors(
+        checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
+        checkedTrackColor: Color = checkedThumbColor,
+        checkedTrackAlpha: Float = 0.54f,
+        uncheckedThumbColor: Color = MaterialTheme.colors.surface,
+        uncheckedTrackColor: Color = MaterialTheme.colors.onSurface,
+        uncheckedTrackAlpha: Float = 0.38f,
+        disabledCheckedThumbColor: Color = checkedThumbColor
+            .copy(alpha = ContentAlpha.disabled)
+            .compositeOver(MaterialTheme.colors.surface),
+        disabledCheckedTrackColor: Color = checkedTrackColor
+            .copy(alpha = ContentAlpha.disabled)
+            .compositeOver(MaterialTheme.colors.surface),
+        disabledUncheckedThumbColor: Color = uncheckedThumbColor
+            .copy(alpha = ContentAlpha.disabled)
+            .compositeOver(MaterialTheme.colors.surface),
+        disabledUncheckedTrackColor: Color = uncheckedTrackColor
+            .copy(alpha = ContentAlpha.disabled)
+            .compositeOver(MaterialTheme.colors.surface)
+    ): SwitchColors = DefaultSwitchColors(
+        checkedThumbColor = checkedThumbColor,
+        checkedTrackColor = checkedTrackColor.copy(alpha = checkedTrackAlpha),
+        uncheckedThumbColor = uncheckedThumbColor,
+        uncheckedTrackColor = uncheckedTrackColor.copy(alpha = uncheckedTrackAlpha),
+        disabledCheckedThumbColor = disabledCheckedThumbColor,
+        disabledCheckedTrackColor = disabledCheckedTrackColor.copy(alpha = checkedTrackAlpha),
+        disabledUncheckedThumbColor = disabledUncheckedThumbColor,
+        disabledUncheckedTrackColor = disabledUncheckedTrackColor.copy(alpha = uncheckedTrackAlpha)
+    )
+}
+
+/**
  * Default [SwitchColors] implementation.
  */
 @OptIn(ExperimentalMaterialApi::class)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
index 63d8945..8d4ed29 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
@@ -37,7 +37,7 @@
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.emptyContent
@@ -143,7 +143,7 @@
     // The color of the Ripple should always the selected color, as we want to show the color
     // before the item is considered selected, and hence before the new contentColor is
     // provided by TabTransition.
-    val ripple = rememberRippleIndication(bounded = false, color = selectedContentColor)
+    val ripple = rememberRipple(bounded = false, color = selectedContentColor)
 
     TabTransition(selectedContentColor, unselectedContentColor, selected) {
         Column(
@@ -165,6 +165,13 @@
 /**
  * Contains default values used by tabs from the Material specification.
  */
+@Deprecated(
+    "TabConstants has been replaced with TabDefaults",
+    ReplaceWith(
+        "TabDefaults",
+        "androidx.compose.material.TabDefaults"
+    )
+)
 object TabConstants {
     /**
      * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
@@ -254,6 +261,98 @@
     val DefaultScrollableTabRowPadding = 52.dp
 }
 
+/**
+ * Contains default values used by tabs from the Material specification.
+ */
+object TabDefaults {
+    /**
+     * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
+     * indicator.
+     *
+     * @param modifier modifier for the divider's layout
+     * @param thickness thickness of the divider
+     * @param color color of the divider
+     */
+    @Composable
+    fun Divider(
+        modifier: Modifier = Modifier,
+        thickness: Dp = DividerThickness,
+        color: Color = AmbientContentColor.current.copy(alpha = DividerOpacity)
+    ) {
+        androidx.compose.material.Divider(modifier = modifier, thickness = thickness, color = color)
+    }
+
+    /**
+     * Default indicator, which will be positioned at the bottom of the [TabRow], on top of the
+     * divider.
+     *
+     * @param modifier modifier for the indicator's layout
+     * @param height height of the indicator
+     * @param color color of the indicator
+     */
+    @Composable
+    fun Indicator(
+        modifier: Modifier = Modifier,
+        height: Dp = IndicatorHeight,
+        color: Color = AmbientContentColor.current
+    ) {
+        Box(
+            modifier
+                .fillMaxWidth()
+                .preferredHeight(height)
+                .background(color = color)
+        )
+    }
+
+    /**
+     * [Modifier] that takes up all the available width inside the [TabRow], and then animates
+     * the offset of the indicator it is applied to, depending on the [currentTabPosition].
+     *
+     * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to
+     * calculate the offset of the indicator this modifier is applied to, as well as its width.
+     */
+    fun Modifier.tabIndicatorOffset(
+        currentTabPosition: TabPosition
+    ): Modifier = composed(
+        inspectorInfo = debugInspectorInfo {
+            name = "tabIndicatorOffset"
+            value = currentTabPosition
+        }
+    ) {
+        // TODO: should we animate the width of the indicator as it moves between tabs of different
+        // sizes inside a scrollable tab row?
+        val currentTabWidth = currentTabPosition.width
+        val indicatorOffset = animate(
+            target = currentTabPosition.left,
+            animSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
+        )
+        fillMaxWidth()
+            .wrapContentSize(Alignment.BottomStart)
+            .offset(x = indicatorOffset)
+            .preferredWidth(currentTabWidth)
+    }
+
+    /**
+     * Default opacity for the color of [Divider]
+     */
+    const val DividerOpacity = 0.12f
+
+    /**
+     * Default thickness for [Divider]
+     */
+    val DividerThickness = 1.dp
+
+    /**
+     * Default height for [Indicator]
+     */
+    val IndicatorHeight = 2.dp
+
+    /**
+     * The default padding from the starting edge before a tab in a [ScrollableTabRow].
+     */
+    val ScrollableTabRowPadding = 52.dp
+}
+
 private val TabTintColor = ColorPropKey()
 
 /**
@@ -396,7 +495,7 @@
 
     // Total offset between the last text baseline and the bottom of the Tab layout
     val totalOffset = with(density) {
-        baselineOffset.toIntPx() + TabConstants.DefaultIndicatorHeight.toIntPx()
+        baselineOffset.toIntPx() + TabDefaults.IndicatorHeight.toIntPx()
     }
 
     val textPlaceableY = tabHeight - lastBaseline - totalOffset
@@ -425,7 +524,7 @@
 
     // Total offset between the last text baseline and the bottom of the Tab layout
     val textOffset = with(density) {
-        baselineOffset.toIntPx() + TabConstants.DefaultIndicatorHeight.toIntPx()
+        baselineOffset.toIntPx() + TabDefaults.IndicatorHeight.toIntPx()
     }
 
     // How much space there is between the top of the icon (essentially the top of this layout)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
index 0cd8808..04c42c9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
@@ -16,12 +16,15 @@
 
 package androidx.compose.material
 
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.material.TabConstants.defaultTabIndicatorOffset
+import androidx.compose.material.TabDefaults.tabIndicatorOffset
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.remember
@@ -66,7 +69,7 @@
  *
  * @sample androidx.compose.material.samples.FancyIndicator
  *
- * We can reuse [TabConstants.defaultTabIndicatorOffset] and just provide this indicator,
+ * We can reuse [TabDefaults.tabIndicatorOffset] and just provide this indicator,
  * as we aren't changing how the size and position of the indicator changes between tabs:
  *
  * @sample androidx.compose.material.samples.FancyIndicatorTabs
@@ -93,9 +96,9 @@
  * Defaults to either the matching `onFoo` color for [backgroundColor], or if [backgroundColor] is
  * not a color from the theme, this will keep the same value set above this TabRow.
  * @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabConstants.DefaultIndicator], using a [TabConstants.defaultTabIndicatorOffset]
+ * will be a [TabDefaults.Indicator], using a [TabDefaults.tabIndicatorOffset]
  * modifier to animate its position. Note that this indicator will be forced to fill up the
- * entire TabRow, so you should use [TabConstants.defaultTabIndicatorOffset] or similar to
+ * entire TabRow, so you should use [TabDefaults.tabIndicatorOffset] or similar to
  * animate the actual drawn indicator inside this space, and provide an offset from the start.
  * @param divider the divider displayed at the bottom of the TabRow. This provides a layer of
  * separation between the TabRow and the content displayed underneath.
@@ -110,12 +113,12 @@
     backgroundColor: Color = MaterialTheme.colors.primarySurface,
     contentColor: Color = contentColorFor(backgroundColor),
     indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = { tabPositions ->
-        TabConstants.DefaultIndicator(
-            Modifier.defaultTabIndicatorOffset(tabPositions[selectedTabIndex])
+        TabDefaults.Indicator(
+            Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
         )
     },
     divider: @Composable () -> Unit = {
-        TabConstants.DefaultDivider()
+        TabDefaults.Divider()
     },
     tabs: @Composable () -> Unit
 ) {
@@ -173,9 +176,9 @@
  * the tabs inside the ScrollableTabRow. This padding helps inform the user that this tab row can
  * be scrolled, unlike a [TabRow].
  * @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabConstants.DefaultIndicator], using a [TabConstants.defaultTabIndicatorOffset]
+ * will be a [TabDefaults.Indicator], using a [TabDefaults.tabIndicatorOffset]
  * modifier to animate its position. Note that this indicator will be forced to fill up the
- * entire ScrollableTabRow, so you should use [TabConstants.defaultTabIndicatorOffset] or similar to
+ * entire ScrollableTabRow, so you should use [TabDefaults.tabIndicatorOffset] or similar to
  * animate the actual drawn indicator inside this space, and provide an offset from the start.
  * @param divider the divider displayed at the bottom of the ScrollableTabRow. This provides a layer
  * of separation between the ScrollableTabRow and the content displayed underneath.
@@ -189,14 +192,14 @@
     modifier: Modifier = Modifier,
     backgroundColor: Color = MaterialTheme.colors.primarySurface,
     contentColor: Color = contentColorFor(backgroundColor),
-    edgePadding: Dp = TabConstants.DefaultScrollableTabRowPadding,
+    edgePadding: Dp = TabDefaults.ScrollableTabRowPadding,
     indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = { tabPositions ->
-        TabConstants.DefaultIndicator(
-            Modifier.defaultTabIndicatorOffset(tabPositions[selectedTabIndex])
+        TabDefaults.Indicator(
+            Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
         )
     },
     divider: @Composable () -> Unit = {
-        TabConstants.DefaultDivider()
+        TabDefaults.Divider()
     },
     tabs: @Composable () -> Unit
 ) {
@@ -305,7 +308,7 @@
                 // Scrolls to the tab with [tabPosition], trying to place it in the center of the
                 // screen or as close to the center as possible.
                 val calculatedOffset = it.calculateTabOffset(density, edgeOffset, tabPositions)
-                scrollState.smoothScrollTo(calculatedOffset)
+                scrollState.smoothScrollTo(calculatedOffset, spec = ScrollableTabRowScrollSpec)
             }
         }
     }
@@ -334,3 +337,11 @@
 }
 
 private val ScrollableTabRowMinimumTabWidth = 90.dp
+
+/**
+ * [AnimationSpec] used when scrolling to a tab that is not fully visible.
+ */
+private val ScrollableTabRowScrollSpec: AnimationSpec<Float> = tween(
+    durationMillis = 250,
+    easing = FastOutSlowInEasing
+)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
index 78e0f16..8274bca5 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
@@ -25,6 +25,7 @@
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.transition
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -35,16 +36,10 @@
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
-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.draw.alpha
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
@@ -77,10 +72,7 @@
  * Implementation of the [TextField] and [OutlinedTextField]
  */
 @Composable
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalFoundationApi::class)
 internal fun TextFieldImpl(
     type: TextFieldType,
     value: TextFieldValue,
@@ -113,7 +105,7 @@
 
     val keyboardController: Ref<SoftwareKeyboardController> = remember { Ref() }
 
-    var isFocused by remember { mutableStateOf(false) }
+    val isFocused = interactionState.contains(Interaction.Focused)
     val inputState = when {
         isFocused -> InputPhase.Focused
         value.text.isEmpty() -> InputPhase.UnfocusedEmpty
@@ -135,6 +127,7 @@
                 visualTransformation = visualTransformation,
                 keyboardOptions = keyboardOptions,
                 maxLines = maxLines,
+                interactionState = interactionState,
                 onImeActionPerformed = {
                     onImeActionPerformed(it, keyboardController.value)
                 },
@@ -150,14 +143,13 @@
     val focusRequester = FocusRequester()
     val textFieldModifier = modifier
         .focusRequester(focusRequester)
-        .focusObserver { isFocused = it.isFocused }
         .let {
             it.clickable(interactionState = interactionState, indication = null) {
                 focusRequester.requestFocus()
                 // TODO(b/163109449): Showing and hiding keyboard should be handled by BaseTextField.
                 //  The requestFocus() call here should be enough to trigger the software keyboard.
                 //  Investiate why this is needed here. If it is really needed, instead of doing
-                //  this in the onClick callback, we should move this logic to the focusObserver
+                //  this in the onClick callback, we should move this logic to onFocusChanged
                 //  so that it can show or hide the keyboard based on the focus state.
                 keyboardController.value?.showSoftwareKeyboard()
             }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
deleted file mode 100644
index 1dfa397..0000000
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:OptIn(ExperimentalMaterialApi::class)
-
-package androidx.compose.material.ripple
-
-import androidx.compose.foundation.Interaction
-import androidx.compose.material.AmbientContentColor
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.luminance
-
-/**
- * Defines the appearance and the behavior for [RippleIndication]s.
- *
- * You can define a new theme and apply it via [AmbientRippleTheme].
- */
-@ExperimentalMaterialApi
-interface RippleTheme {
-    /**
-     * @return the default [RippleIndication] color at the call site's position in the hierarchy.
-     * This color will be used when a color is not explicitly set in the [RippleIndication] itself.
-     */
-    @Composable
-    fun defaultColor(): Color
-
-    /**
-     * @return the [RippleOpacity] used to calculate the opacity for the ripple depending on the
-     * [Interaction] for a given component. This will be set as the alpha channel for
-     * [defaultColor] or the color explicitly provided to the [RippleIndication].
-     */
-    @Composable
-    fun rippleOpacity(): RippleOpacity
-}
-
-// TODO: can be a fun interface when we rebase to use Kotlin 1.4
-/**
- * RippleOpacity defines the opacity of the ripple / state layer for a given [Interaction].
- */
-@ExperimentalMaterialApi
-interface RippleOpacity {
-    /**
-     * @return the opacity of the ripple for the given [interaction]. Return `0f` if this
-     * particular interaction should not show a corresponding ripple / state layer.
-     */
-    fun opacityForInteraction(interaction: Interaction): Float
-}
-
-/**
- * Ambient used for providing [RippleTheme] down the tree.
- */
-@ExperimentalMaterialApi
-val AmbientRippleTheme = staticAmbientOf<RippleTheme> { DefaultRippleTheme }
-
-private object DefaultRippleTheme : RippleTheme {
-    @Composable
-    override fun defaultColor(): Color {
-        val contentColor = AmbientContentColor.current
-        val lightTheme = MaterialTheme.colors.isLight
-        val contentLuminance = contentColor.luminance()
-        // If we are on a colored surface (typically indicated by low luminance content), the
-        // ripple color should be white.
-        return if (!lightTheme && contentLuminance < 0.5) {
-            Color.White
-            // Otherwise use contentColor
-        } else {
-            contentColor
-        }
-    }
-
-    @Composable
-    override fun rippleOpacity(): RippleOpacity {
-        val lightTheme = MaterialTheme.colors.isLight
-        val contentLuminance = AmbientContentColor.current.luminance()
-        return when {
-            lightTheme -> {
-                if (contentLuminance > 0.5) {
-                    LightThemeHighContrastRippleOpacity
-                } else {
-                    LightThemeReducedContrastRippleOpacity
-                }
-            }
-            else -> {
-                DarkThemeRippleOpacity
-            }
-        }
-    }
-}
-
-@Suppress("unused")
-private sealed class DefaultRippleOpacity(
-    val pressed: Float,
-    val focused: Float,
-    val dragged: Float,
-    val hovered: Float
-) : RippleOpacity {
-    override fun opacityForInteraction(interaction: Interaction): Float = when (interaction) {
-        Interaction.Pressed -> pressed
-        Interaction.Dragged -> dragged
-        else -> 0f
-    }
-}
-
-/**
- * Opacity values for high luminance content in a light theme.
- *
- * This content will typically be placed on colored surfaces, so it is important that the
- * contrast here is higher to meet accessibility standards, and increase legibility.
- *
- * These levels are typically used for text / iconography in primary colored tabs /
- * bottom navigation / etc.
- */
-private object LightThemeHighContrastRippleOpacity : DefaultRippleOpacity(
-    pressed = 0.24f,
-    focused = 0.24f,
-    dragged = 0.16f,
-    hovered = 0.08f
-)
-
-/**
- * Alpha levels for low luminance content in a light theme.
- *
- * This content will typically be placed on grayscale surfaces, so the contrast here can be lower
- * without sacrificing accessibility and legibility.
- *
- * These levels are typically used for body text on the main surface (white in light theme, grey
- * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc.
- */
-private object LightThemeReducedContrastRippleOpacity : DefaultRippleOpacity(
-    pressed = 0.12f,
-    focused = 0.12f,
-    dragged = 0.08f,
-    hovered = 0.04f
-)
-
-/**
- * Alpha levels for all content in a dark theme.
- */
-private object DarkThemeRippleOpacity : DefaultRippleOpacity(
-    pressed = 0.10f,
-    focused = 0.12f,
-    dragged = 0.08f,
-    hovered = 0.04f
-)
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt b/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
index 764c108..853e0ac 100644
--- a/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
+++ b/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
@@ -66,38 +66,50 @@
         }.toTypedArray()
     }
 
+    /**
+     * In light theme the default background color is white, with black text and a medium content
+     * alpha of 74%.
+     */
     @Test
-    fun assertContrast_blackTextOnWhiteBackground() {
+    fun assertContrast_lightTheme_backgroundBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.Black,
+            textAlpha = 0.74f,
             backgroundColor = Color.White
         )
     }
 
+    /**
+     * In dark theme the default background color is #121212, with white text and a medium content
+     * alpha of 60%.
+     */
     @Test
-    fun assertContrast_whiteTextOnBlackBackground() {
+    fun assertContrast_darkTheme_backgroundBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.White,
-            backgroundColor = Color.Black
+            textAlpha = 0.6f,
+            backgroundColor = Color(0xFF121212)
         )
     }
 
     @Test
-    fun assertContrast_blackTextOnPrimaryBackground() {
+    fun assertContrast_lightTheme_primaryBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.Black,
+            textAlpha = 0.74f,
             backgroundColor = color
         )
     }
 
     @Test
-    fun assertContrast_whiteTextOnPrimaryBackground() {
+    fun assertContrast_darkTheme_primaryBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.White,
+            textAlpha = 0.6f,
             backgroundColor = color
         )
     }
@@ -106,11 +118,13 @@
 private fun assertContrastRatio(
     selectionColor: Color,
     textColor: Color,
+    textAlpha: Float,
     backgroundColor: Color
 ) {
+    val textColorWithAlpha = textColor.copy(alpha = textAlpha)
     val selectionBackgroundColor = calculateSelectionBackgroundColor(
         selectionColor = selectionColor,
-        textColor = textColor,
+        textColor = textColorWithAlpha,
         backgroundColor = backgroundColor
     )
 
@@ -118,7 +132,7 @@
     // using the default alpha for consistency with the spec.
     val minimumCompositeBackground = selectionBackgroundColor.copy(alpha = MinimumSelectionAlpha)
         .compositeOver(backgroundColor)
-    val minimumCompositeTextColor = textColor.compositeOver(minimumCompositeBackground)
+    val minimumCompositeTextColor = textColorWithAlpha.compositeOver(minimumCompositeBackground)
     val minimumContrastRatio = calculateContrastRatio(
         foreground = minimumCompositeTextColor,
         background = minimumCompositeBackground
@@ -131,7 +145,7 @@
 
     // Otherwise, ensure that the value we choose is accessible
     val compositeBackground = selectionBackgroundColor.compositeOver(backgroundColor)
-    val compositeTextColor = textColor.compositeOver(compositeBackground)
+    val compositeTextColor = textColorWithAlpha.compositeOver(compositeBackground)
     val contrastRatio = calculateContrastRatio(
         foreground = compositeTextColor,
         background = compositeBackground
@@ -143,7 +157,7 @@
         // If we searched for a new alpha, this alpha should be the maximal alpha that meets
         // contrast requirements, so the contrast ratio should be just above 4.5f in order to
         // maximize alpha
-        assertThat(contrastRatio).isWithin(0.05f).of(RequiredContrastRatio)
+        assertThat(contrastRatio).isWithin(0.1f).of(RequiredContrastRatio)
     }
 }
 
diff --git a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
index 8cb5760..711bd90 100644
--- a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
+++ b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
@@ -18,6 +18,7 @@
 
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.yield
+import java.awt.DisplayMode
 import java.awt.GraphicsEnvironment
 
 // TODO implement local Recomposer in each Window, so each Window can have own MonotonicFrameClock.
@@ -32,14 +33,17 @@
             if (GraphicsEnvironment.isHeadless()) {
                 yield()
             } else {
-                val defaultRefreshRate = GraphicsEnvironment
-                    .getLocalGraphicsEnvironment()
-                    .defaultScreenDevice
-                    .displayMode
-                    .refreshRate
-                delay(1000L / defaultRefreshRate)
+                delay(1000L / getFramesPerSecond())
             }
             return onFrame(System.nanoTime())
         }
+
+        private fun getFramesPerSecond(): Int {
+            val refreshRate = GraphicsEnvironment
+                .getLocalGraphicsEnvironment()
+                .screenDevices.maxOfOrNull { it.displayMode.refreshRate }
+                ?: DisplayMode.REFRESH_RATE_UNKNOWN
+            return if (refreshRate != DisplayMode.REFRESH_RATE_UNKNOWN) refreshRate else 60
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
index dba48a2..704b120 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
@@ -143,7 +143,7 @@
             "Primary composable lambda parameter not named `content`",
             "Composable functions with only one composable lambda parameter should use the name " +
                 "`content` for the parameter.",
-            Category.CORRECTNESS, 3, Severity.WARNING,
+            Category.CORRECTNESS, 3, Severity.IGNORE,
             Implementation(
                 ComposableLambdaParameterDetector::class.java,
                 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
@@ -156,7 +156,7 @@
             "Composable functions with only one composable lambda parameter should place the " +
                 "parameter at the end of the parameter list, so it can be used as a trailing " +
                 "lambda.",
-            Category.CORRECTNESS, 3, Severity.WARNING,
+            Category.CORRECTNESS, 3, Severity.IGNORE,
             Implementation(
                 ComposableLambdaParameterDetector::class.java,
                 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt
new file mode 100644
index 0000000..d05d0b1
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UCallExpression
+import java.util.EnumSet
+
+/**
+ * [Detector] that checks `remember` calls to make sure they are not returning [Unit].
+ */
+class RememberDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
+
+    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+        override fun visitCallExpression(node: UCallExpression) {
+            val call = node.resolve() ?: return
+            if (call.name == RememberShortName &&
+                (call.containingFile as? PsiJavaFile)?.packageName == RuntimePackageName
+            ) {
+                if (node.getExpressionType() == PsiType.VOID) {
+                    context.report(
+                        RememberReturnType,
+                        node,
+                        context.getNameLocation(node),
+                        "`remember` calls must not return `Unit`"
+                    )
+                }
+            }
+        }
+    }
+
+    companion object {
+        val RememberReturnType = Issue.create(
+            "RememberReturnType",
+            "`remember` calls must not return `Unit`",
+            "A call to `remember` that returns `Unit` is always an error. This typically happens " +
+                "when using `remember` to mutate variables on an object. `remember` is executed " +
+                "during the composition, which means that if the composition fails or is " +
+                "happening on a separate thread, the mutated variables may not reflect the true " +
+                "state of the composition. Instead, use `SideEffect` to make deferred changes " +
+                "once the composition succeeds, or mutate `MutableState` backed variables " +
+                "directly, as these will handle composition failure for you.",
+            Category.CORRECTNESS, 3, Severity.ERROR,
+            Implementation(
+                RememberDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+            )
+        )
+    }
+}
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
index 7ab6018..6d5dcad 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
@@ -30,6 +30,7 @@
         AmbientNamingDetector.AmbientNaming,
         ComposableLambdaParameterDetector.ComposableLambdaParameterNaming,
         ComposableLambdaParameterDetector.ComposableLambdaParameterPosition,
-        ComposableNamingDetector.ComposableNaming
+        ComposableNamingDetector.ComposableNaming,
+        RememberDetector.RememberReturnType
     )
 }
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
index 33d78b0..6d8f4d6 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
@@ -25,5 +25,9 @@
 @Suppress("DEPRECATION")
 val UMethod.isComposable get() = annotations.any { it.qualifiedName == ComposableFqn }
 
-const val ComposableFqn = "androidx.compose.runtime.Composable"
-val ComposableShortName = ComposableFqn.split(".").last()
\ No newline at end of file
+const val RuntimePackageName = "androidx.compose.runtime"
+
+const val ComposableFqn = "$RuntimePackageName.Composable"
+val ComposableShortName = ComposableFqn.split(".").last()
+
+const val RememberShortName = "remember"
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
new file mode 100644
index 0000000..06f8db1
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+
+/**
+ * Test for [RememberDetector].
+ */
+class RememberDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = RememberDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(RememberDetector.RememberReturnType)
+
+    @Test
+    fun returnsUnit() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.runtime.foo
+
+                import androidx.compose.runtime.Composable
+                import androidx.compose.runtime.remember
+
+                class FooState {
+                    fun update(new: Int) {}
+                }
+
+                @Composable
+                fun Test() {
+                    val state = remember { FooState() }
+                    remember {
+                        state.update(5)
+                    }
+                    val unit = remember {
+                        state.update(5)
+                    }
+                }
+
+                @Composable
+                fun Test(number: Int) {
+                    val state = remember { FooState() }
+                    remember(number) {
+                        state.update(number)
+                    }
+                    val unit = remember(number) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int) {
+                    val state = remember { FooState() }
+                    remember(number1, number2) {
+                        state.update(number)
+                    }
+                    val unit = remember(number1, number2) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int, number3: Int) {
+                    val state = remember { FooState() }
+                    remember(number1, number2, number3) {
+                        state.update(number)
+                    }
+                    val unit = remember(number1, number2, number3) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int, number3: Int, flag: Boolean) {
+                    val state = remember { FooState() }
+                    remember(number1, number2, number3, flag) {
+                        state.update(number)
+                    }
+                    val unit = remember(number1, number2, number3, flag) {
+                        state.update(number)
+                    }
+                }
+            """
+            ),
+            composableStub,
+            rememberStub
+        )
+            .run()
+            .expect(
+                """
+src/androidx/compose/runtime/foo/FooState.kt:14: Error: remember calls must not return Unit [RememberReturnType]
+                    remember {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:17: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit = remember {
+                               ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:25: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:28: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit = remember(number) {
+                               ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:36: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number1, number2) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:39: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit = remember(number1, number2) {
+                               ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:47: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number1, number2, number3) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:50: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit = remember(number1, number2, number3) {
+                               ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:58: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number1, number2, number3, flag) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:61: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit = remember(number1, number2, number3, flag) {
+                               ~~~~~~~~
+10 errors, 0 warnings
+            """
+            )
+    }
+
+    @Test
+    fun returnsValue_explicitUnitType() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.runtime.foo
+
+                import androidx.compose.runtime.Composable
+                import androidx.compose.runtime.remember
+
+                class FooState {
+                    fun update(new: Int): Boolean = true
+                }
+
+                @Composable
+                fun Test() {
+                    val state = remember { FooState() }
+                    remember<Unit> {
+                        state.update(5)
+                    }
+                    val result = remember<Unit> {
+                        state.update(5)
+                    }
+                }
+
+                @Composable
+                fun Test(number: Int) {
+                    val state = remember { FooState() }
+                    remember<Unit>(number) {
+                        state.update(number)
+                    }
+                    val result = remember<Unit>(number) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int) {
+                    val state = remember { FooState() }
+                    remember<Unit>(number1, number2) {
+                        state.update(number)
+                    }
+                    val result = remember<Unit>(number1, number2) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int, number3: Int) {
+                    val state = remember { FooState() }
+                    remember<Unit>(number1, number2, number3) {
+                        state.update(number)
+                    }
+                    val result = remember<Unit>(number1, number2, number3) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int, number3: Int, flag: Boolean) {
+                    val state = remember { FooState() }
+                    remember<Unit>(number1, number2, number3, flag) {
+                        state.update(number)
+                    }
+                    val result = remember<Unit>(number1, number2, number3, flag) {
+                        state.update(number)
+                    }
+                }
+            """
+            ),
+            composableStub,
+            rememberStub
+        )
+            .run()
+            .expect(
+                """
+src/androidx/compose/runtime/foo/FooState.kt:14: Error: remember calls must not return Unit [RememberReturnType]
+                    remember<Unit> {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:17: Error: remember calls must not return Unit [RememberReturnType]
+                    val result = remember<Unit> {
+                                 ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:25: Error: remember calls must not return Unit [RememberReturnType]
+                    remember<Unit>(number) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:28: Error: remember calls must not return Unit [RememberReturnType]
+                    val result = remember<Unit>(number) {
+                                 ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:36: Error: remember calls must not return Unit [RememberReturnType]
+                    remember<Unit>(number1, number2) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:39: Error: remember calls must not return Unit [RememberReturnType]
+                    val result = remember<Unit>(number1, number2) {
+                                 ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:47: Error: remember calls must not return Unit [RememberReturnType]
+                    remember<Unit>(number1, number2, number3) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:50: Error: remember calls must not return Unit [RememberReturnType]
+                    val result = remember<Unit>(number1, number2, number3) {
+                                 ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:58: Error: remember calls must not return Unit [RememberReturnType]
+                    remember<Unit>(number1, number2, number3, flag) {
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:61: Error: remember calls must not return Unit [RememberReturnType]
+                    val result = remember<Unit>(number1, number2, number3, flag) {
+                                 ~~~~~~~~
+10 errors, 0 warnings
+            """
+            )
+    }
+
+    @Test
+    fun noErrors() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.runtime.foo
+
+                import androidx.compose.runtime.Composable
+                import androidx.compose.runtime.remember
+
+                class FooState {
+                    fun update(new: Int): Boolean = true
+                }
+
+                @Composable
+                fun Test() {
+                    val state = remember { FooState() }
+                    remember {
+                        state.update(5)
+                    }
+                    val result = remember {
+                        state.update(5)
+                    }
+                }
+
+                @Composable
+                fun Test(number: Int) {
+                    val state = remember { FooState() }
+                    remember(number) {
+                        state.update(number)
+                    }
+                    val result = remember(number) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int) {
+                    val state = remember { FooState() }
+                    remember(number1, number2) {
+                        state.update(number)
+                    }
+                    val result = remember(number1, number2) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int, number3: Int) {
+                    val state = remember { FooState() }
+                    remember(number1, number2, number3) {
+                        state.update(number)
+                    }
+                    val result = remember(number1, number2, number3) {
+                        state.update(number)
+                    }
+                }
+
+                @Composable
+                fun Test(number1: Int, number2: Int, number3: Int, flag: Boolean) {
+                    val state = remember { FooState() }
+                    remember(number1, number2, number3, flag) {
+                        state.update(number)
+                    }
+                    val result = remember(number1, number2, number3, flag) {
+                        state.update(number)
+                    }
+                }
+            """
+            ),
+            composableStub,
+            rememberStub
+        )
+            .run()
+            .expectClean()
+    }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
index 92428e8..9c086d2 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
@@ -34,4 +34,42 @@
         )
         annotation class Composable
     """
-)
\ No newline at end of file
+)
+
+val rememberStub: LintDetectorTest.TestFile = LintDetectorTest.kotlin(
+"""
+        package androidx.compose.runtime
+
+        import androidx.compose.runtime.Composable
+
+        @Composable
+        inline fun <T> remember(calculation: () -> T): T = calculation()
+
+        @Composable
+        inline fun <T, V1> remember(
+            v1: V1,
+            calculation: () -> T
+        ): T = calculation()
+
+        @Composable
+        inline fun <T, V1, V2> remember(
+            v1: V1,
+            v2: V2,
+            calculation: () -> T
+        ): T = calculation()
+
+        @Composable
+        inline fun <T, V1, V2, V3> remember(
+            v1: V1,
+            v2: V2,
+            v3: V3,
+            calculation: () -> T
+        ): T = calculation()
+
+        @Composable
+        inline fun <V> remember(
+            vararg inputs: Any?,
+            calculation: () -> V
+        ): V = calculation()
+    """
+)
diff --git a/compose/runtime/runtime-rxjava3/samples/build.gradle b/compose/runtime/runtime-rxjava3/samples/build.gradle
index ac887e3..1711449 100644
--- a/compose/runtime/runtime-rxjava3/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava3/samples/build.gradle
@@ -32,7 +32,7 @@
     kotlinPlugin project(":compose:compiler:compiler")
 
     implementation(KOTLIN_STDLIB)
-    implementation project(":annotation:annotation-sampled")
+    compileOnly project(":annotation:annotation-sampled")
     implementation project(":compose:foundation:foundation")
     implementation project(":compose:material:material")
     implementation project(":compose:runtime:runtime-rxjava3")
diff --git a/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt b/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt
index ad90774..f72bd7f 100644
--- a/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt
+++ b/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt
@@ -104,7 +104,7 @@
                 content = content
             )
             onActive {
-                require(key !in registryHolders)
+                require(key !in registryHolders) { "Key $key was used multiple times " }
                 savedStates -= key
                 registryHolders[key] = registryHolder
                 onDispose {
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 835bdc3..ebe4b82 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.compose.runtime {
 
-  @androidx.compose.runtime.ExperimentalComposeApi public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
+  public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
     ctor public AbstractApplier(T? root);
     method public final void clear();
     method public void down(T? node);
@@ -24,8 +24,8 @@
   }
 
   @androidx.compose.runtime.Stable public abstract sealed class Ambient<T> {
-    method public final inline T! getCurrent();
-    property public final inline T! current;
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! getCurrent();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! current;
   }
 
   public final class AmbientKt {
@@ -34,18 +34,12 @@
     method public static <T> androidx.compose.runtime.ProvidableAmbient<T> staticAmbientOf(optional kotlin.jvm.functions.Function0<? extends T>? defaultFactory);
   }
 
-  @androidx.compose.runtime.InternalComposeApi public final class Anchor {
-    method public boolean getValid();
-    method public int toIndexFor(androidx.compose.runtime.SlotTable slots);
-    method public int toIndexFor(androidx.compose.runtime.SlotWriter writer);
-    property public final boolean valid;
-  }
-
-  @androidx.compose.runtime.ExperimentalComposeApi public interface Applier<N> {
+  public interface Applier<N> {
     method public void clear();
     method public void down(N? node);
     method public N! getCurrent();
-    method public void insert(int index, N? instance);
+    method public void insertBottomUp(int index, N? instance);
+    method public void insertTopDown(int index, N? instance);
     method public void move(int from, int to, int count);
     method public default void onBeginChanges();
     method public default void onEndChanges();
@@ -61,10 +55,10 @@
     method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
   }
 
-  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
   }
 
-  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
     method public abstract boolean preventCapture() default false;
     method public abstract boolean readonly() default false;
     method public abstract boolean restartable() default true;
@@ -84,7 +78,7 @@
   }
 
   public final class Composer<N> {
-    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+    ctor public Composer(@kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
     method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
     method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -97,23 +91,18 @@
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(double value);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
     method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
+    method @androidx.compose.runtime.InternalComposeApi public void collectParameterInformation();
     method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
-    method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
-    method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
     method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
-    method public androidx.compose.runtime.Applier<N> getApplier();
     method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
+    method public androidx.compose.runtime.CompositionData getCompositionData();
     method public int getCurrentCompoundKeyHash();
     method public boolean getDefaultsInvalid();
-    method public boolean getInserting();
     method public boolean getSkipping();
-    method public androidx.compose.runtime.SlotTable getSlotTable();
     method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
-    method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
     method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
     method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
     method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -123,23 +112,20 @@
     method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
-    property public final androidx.compose.runtime.Applier<N> applier;
+    method @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
     property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
+    property public final androidx.compose.runtime.CompositionData compositionData;
     property public final int currentCompoundKeyHash;
     property public final boolean defaultsInvalid;
-    property public final boolean inserting;
     property public final boolean skipping;
-    property public final androidx.compose.runtime.SlotTable slotTable;
   }
 
   public final class ComposerKt {
-    method public static androidx.compose.runtime.Composer<?> getCurrentComposer();
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.Composer<?> getCurrentComposer();
   }
 
   public interface Composition {
@@ -148,6 +134,24 @@
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public interface CompositionData {
+    method public Iterable<androidx.compose.runtime.CompositionGroup> getCompositionGroups();
+    method public boolean isEmpty();
+    property public abstract Iterable<androidx.compose.runtime.CompositionGroup> compositionGroups;
+    property public abstract boolean isEmpty;
+  }
+
+  public interface CompositionGroup extends androidx.compose.runtime.CompositionData {
+    method public Iterable<java.lang.Object> getData();
+    method public Object getKey();
+    method public Object? getNode();
+    method public String? getSourceInfo();
+    property public abstract Iterable<java.lang.Object> data;
+    property public abstract Object key;
+    property public abstract Object? node;
+    property public abstract String? sourceInfo;
+  }
+
   public final class CompositionKt {
     method @androidx.compose.runtime.ExperimentalComposeApi public static androidx.compose.runtime.Composition compositionFor(Object key, androidx.compose.runtime.Applier<?> applier, androidx.compose.runtime.CompositionReference parent, optional kotlin.jvm.functions.Function0<kotlin.Unit> onCreated);
   }
@@ -178,7 +182,7 @@
 
   public final class EffectsKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.CompositionReference compositionReference();
-    method public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
+    method @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
     method @androidx.compose.runtime.Composable public static void onActive(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
     method @androidx.compose.runtime.Composable public static inline void onCommit(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
     method @androidx.compose.runtime.Composable public static <V1> void onCommit(V1? v1, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
@@ -273,19 +277,6 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
   }
 
-  public final class ObserverMap<K, V> {
-    ctor public ObserverMap();
-    method public void add(K key, V value);
-    method public void clear();
-    method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
-    method public boolean contains(K key, V value);
-    method public operator java.util.List<V> get(Iterable<? extends K> keys);
-    method public java.util.List<V> getValueOf(K key);
-    method public void remove(K key);
-    method public void remove(K key, V value);
-    method public void removeValue(V value);
-  }
-
   public final class ProduceStateKt {
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? subject, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
@@ -359,158 +350,13 @@
   }
 
   public final class SkippableUpdater<T> {
-    ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
-  }
-
-  @androidx.compose.runtime.InternalComposeApi public final class SlotReader {
-    ctor public SlotReader(androidx.compose.runtime.SlotTable table);
-    method public androidx.compose.runtime.Anchor anchor(optional int index);
-    method public void beginEmpty();
-    method public void close();
-    method public void endEmpty();
-    method public void endGroup();
-    method public java.util.List<androidx.compose.runtime.KeyInfo> extractKeys();
-    method public Object? get(int index);
-    method public int getCurrentEnd();
-    method public int getCurrentGroup();
-    method public Object! getGroupAux();
-    method public int getGroupEnd();
-    method public int getGroupKey();
-    method public Object! getGroupNode();
-    method public Object! getGroupObjectKey();
-    method public int getGroupSize();
-    method public int getGroupSlotCount();
-    method public int getGroupSlotIndex();
-    method public boolean getInEmpty();
-    method public int getNodeCount();
-    method public int getParent();
-    method public int getParentNodes();
-    method public int getSize();
-    method public int getSlot();
-    method public Object? groupAux(int index);
-    method public int groupEnd(int index);
-    method public Object? groupGet(int index);
-    method public int groupKey(int index);
-    method public int groupKey(androidx.compose.runtime.Anchor anchor);
-    method public Object? groupObjectKey(int index);
-    method public int groupSize(int index);
-    method public boolean hasObjectKey(int index);
-    method public boolean isGroupEnd();
-    method public boolean isNode();
-    method public boolean isNode(int index);
-    method public Object? next();
-    method public Object? node(int index);
-    method public int nodeCount(int index);
-    method public int parent(int index);
-    method public int parentOf(int index);
-    method public void reposition(int index);
-    method public void restoreParent(int index);
-    method public int skipGroup();
-    method public void skipToGroupEnd();
-    method public void startGroup();
-    method public void startNode();
-    property public final int currentEnd;
-    property public final int currentGroup;
-    property public final Object! groupAux;
-    property public final int groupEnd;
-    property public final int groupKey;
-    property public final Object! groupNode;
-    property public final Object! groupObjectKey;
-    property public final int groupSize;
-    property public final int groupSlotCount;
-    property public final int groupSlotIndex;
-    property public final boolean inEmpty;
-    property public final boolean isGroupEnd;
-    property public final boolean isNode;
-    property public final int nodeCount;
-    property public final int parent;
-    property public final int parentNodes;
-    property public final int size;
-    property public final int slot;
-  }
-
-  @androidx.compose.runtime.InternalComposeApi public final class SlotTable {
-    ctor public SlotTable();
-    method public int anchorIndex(androidx.compose.runtime.Anchor anchor);
-    method public String asString();
-    method public int[] getGroups();
-    method public int getGroupsSize();
-    method public Object![] getSlots();
-    method public int getSlotsSize();
-    method public boolean isEmpty();
-    method public androidx.compose.runtime.SlotReader openReader();
-    method public androidx.compose.runtime.SlotWriter openWriter();
-    method public boolean ownsAnchor(androidx.compose.runtime.Anchor anchor);
-    method public inline <T> T! read(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.SlotReader,? extends T> block);
-    method public void verifyWellFormed();
-    method public inline <T> T! write(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.SlotWriter,? extends T> block);
-    property public final int[] groups;
-    property public final int groupsSize;
-    property public final boolean isEmpty;
-    property public final Object![] slots;
-    property public final int slotsSize;
   }
 
   public final class SlotTableKt {
     method public static java.util.List<java.lang.Integer> slice(int[], Iterable<java.lang.Integer> indices);
   }
 
-  @androidx.compose.runtime.InternalComposeApi public final class SlotWriter {
-    method public void advanceBy(int amount);
-    method public androidx.compose.runtime.Anchor anchor(optional int index);
-    method public int anchorIndex(androidx.compose.runtime.Anchor anchor);
-    method public void beginInsert();
-    method public void close();
-    method public int endGroup();
-    method public void endInsert();
-    method public void ensureStarted(int index);
-    method public void ensureStarted(androidx.compose.runtime.Anchor anchor);
-    method public boolean getClosed();
-    method public int getCurrentGroup();
-    method public int getParent();
-    method public Object? groupAux(int index);
-    method public int groupKey(int index);
-    method public Object? groupObjectKey(int index);
-    method public int groupSize(int index);
-    method public java.util.Iterator<java.lang.Object> groupSlots();
-    method public String groupsAsString();
-    method public boolean isGroupEnd();
-    method public boolean isNode();
-    method public java.util.List<androidx.compose.runtime.Anchor> moveFrom(androidx.compose.runtime.SlotTable table, int index);
-    method public void moveGroup(int offset);
-    method public Object? node(int index);
-    method public int parent(int index);
-    method public int parent(androidx.compose.runtime.Anchor anchor);
-    method public boolean removeGroup();
-    method public void seek(androidx.compose.runtime.Anchor anchor);
-    method public void set(Object? value);
-    method public Object? set(int index, Object? value);
-    method public Object? skip();
-    method public int skipGroup();
-    method public void skipToGroupEnd();
-    method public void startData(int key, Object? objectKey, Object? aux);
-    method public void startData(int key, Object? aux);
-    method public void startGroup();
-    method public void startGroup(int key);
-    method public void startGroup(int key, Object? dataKey);
-    method public void startNode(Object? key);
-    method public void startNode(Object? key, Object? node);
-    method public Object? update(Object? value);
-    method public void updateAux(Object? value);
-    method public void updateNode(Object? value);
-    method public void updateParentNode(Object? value);
-    property public final boolean closed;
-    property public final int currentGroup;
-    property public final boolean isGroupEnd;
-    property public final boolean isNode;
-    property public final int parent;
-  }
-
   public interface SnapshotMutationPolicy<T> {
     method public boolean equivalent(T? a, T? b);
     method @androidx.compose.runtime.ExperimentalComposeApi public default T? merge(T? previous, T? current, T? applied);
@@ -545,16 +391,11 @@
   }
 
   public final class Updater<T> {
-    ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
     method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
     method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
 }
@@ -698,6 +539,7 @@
   }
 
   public final class LiveLiteralKt {
+    method @androidx.compose.runtime.InternalComposeApi public static void enableLiveLiterals();
     method public static boolean isLiveLiteralsEnabled();
     method @androidx.compose.runtime.InternalComposeApi public static <T> androidx.compose.runtime.State<T> liveLiteral(String key, T? value);
     method @androidx.compose.runtime.InternalComposeApi public static void updateLiveLiteralValue(String key, Object? value);
@@ -909,7 +751,7 @@
 package androidx.compose.runtime.tooling {
 
   public final class InspectionTablesKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<java.util.Set<androidx.compose.runtime.SlotTable>> getInspectionTables();
+    method public static androidx.compose.runtime.ProvidableAmbient<java.util.Set<androidx.compose.runtime.CompositionData>> getInspectionTables();
   }
 
 }
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 835bdc3..ebe4b82 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.compose.runtime {
 
-  @androidx.compose.runtime.ExperimentalComposeApi public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
+  public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
     ctor public AbstractApplier(T? root);
     method public final void clear();
     method public void down(T? node);
@@ -24,8 +24,8 @@
   }
 
   @androidx.compose.runtime.Stable public abstract sealed class Ambient<T> {
-    method public final inline T! getCurrent();
-    property public final inline T! current;
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! getCurrent();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! current;
   }
 
   public final class AmbientKt {
@@ -34,18 +34,12 @@
     method public static <T> androidx.compose.runtime.ProvidableAmbient<T> staticAmbientOf(optional kotlin.jvm.functions.Function0<? extends T>? defaultFactory);
   }
 
-  @androidx.compose.runtime.InternalComposeApi public final class Anchor {
-    method public boolean getValid();
-    method public int toIndexFor(androidx.compose.runtime.SlotTable slots);
-    method public int toIndexFor(androidx.compose.runtime.SlotWriter writer);
-    property public final boolean valid;
-  }
-
-  @androidx.compose.runtime.ExperimentalComposeApi public interface Applier<N> {
+  public interface Applier<N> {
     method public void clear();
     method public void down(N? node);
     method public N! getCurrent();
-    method public void insert(int index, N? instance);
+    method public void insertBottomUp(int index, N? instance);
+    method public void insertTopDown(int index, N? instance);
     method public void move(int from, int to, int count);
     method public default void onBeginChanges();
     method public default void onEndChanges();
@@ -61,10 +55,10 @@
     method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
   }
 
-  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
   }
 
-  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
     method public abstract boolean preventCapture() default false;
     method public abstract boolean readonly() default false;
     method public abstract boolean restartable() default true;
@@ -84,7 +78,7 @@
   }
 
   public final class Composer<N> {
-    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+    ctor public Composer(@kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
     method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
     method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -97,23 +91,18 @@
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(double value);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
     method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
+    method @androidx.compose.runtime.InternalComposeApi public void collectParameterInformation();
     method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
-    method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
-    method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
     method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
-    method public androidx.compose.runtime.Applier<N> getApplier();
     method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
+    method public androidx.compose.runtime.CompositionData getCompositionData();
     method public int getCurrentCompoundKeyHash();
     method public boolean getDefaultsInvalid();
-    method public boolean getInserting();
     method public boolean getSkipping();
-    method public androidx.compose.runtime.SlotTable getSlotTable();
     method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
-    method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
     method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
     method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
     method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -123,23 +112,20 @@
     method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
-    property public final androidx.compose.runtime.Applier<N> applier;
+    method @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
     property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
+    property public final androidx.compose.runtime.CompositionData compositionData;
     property public final int currentCompoundKeyHash;
     property public final boolean defaultsInvalid;
-    property public final boolean inserting;
     property public final boolean skipping;
-    property public final androidx.compose.runtime.SlotTable slotTable;
   }
 
   public final class ComposerKt {
-    method public static androidx.compose.runtime.Composer<?> getCurrentComposer();
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.Composer<?> getCurrentComposer();
   }
 
   public interface Composition {
@@ -148,6 +134,24 @@
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public interface CompositionData {
+    method public Iterable<androidx.compose.runtime.CompositionGroup> getCompositionGroups();
+    method public boolean isEmpty();
+    property public abstract Iterable<androidx.compose.runtime.CompositionGroup> compositionGroups;
+    property public abstract boolean isEmpty;
+  }
+
+  public interface CompositionGroup extends androidx.compose.runtime.CompositionData {
+    method public Iterable<java.lang.Object> getData();
+    method public Object getKey();
+    method public Object? getNode();
+    method public String? getSourceInfo();
+    property public abstract Iterable<java.lang.Object> data;
+    property public abstract Object key;
+    property public abstract Object? node;
+    property public abstract String? sourceInfo;
+  }
+
   public final class CompositionKt {
     method @androidx.compose.runtime.ExperimentalComposeApi public static androidx.compose.runtime.Composition compositionFor(Object key, androidx.compose.runtime.Applier<?> applier, androidx.compose.runtime.CompositionReference parent, optional kotlin.jvm.functions.Function0<kotlin.Unit> onCreated);
   }
@@ -178,7 +182,7 @@
 
   public final class EffectsKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.CompositionReference compositionReference();
-    method public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
+    method @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
     method @androidx.compose.runtime.Composable public static void onActive(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
     method @androidx.compose.runtime.Composable public static inline void onCommit(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
     method @androidx.compose.runtime.Composable public static <V1> void onCommit(V1? v1, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
@@ -273,19 +277,6 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
   }
 
-  public final class ObserverMap<K, V> {
-    ctor public ObserverMap();
-    method public void add(K key, V value);
-    method public void clear();
-    method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
-    method public boolean contains(K key, V value);
-    method public operator java.util.List<V> get(Iterable<? extends K> keys);
-    method public java.util.List<V> getValueOf(K key);
-    method public void remove(K key);
-    method public void remove(K key, V value);
-    method public void removeValue(V value);
-  }
-
   public final class ProduceStateKt {
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? subject, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
@@ -359,158 +350,13 @@
   }
 
   public final class SkippableUpdater<T> {
-    ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
-  }
-
-  @androidx.compose.runtime.InternalComposeApi public final class SlotReader {
-    ctor public SlotReader(androidx.compose.runtime.SlotTable table);
-    method public androidx.compose.runtime.Anchor anchor(optional int index);
-    method public void beginEmpty();
-    method public void close();
-    method public void endEmpty();
-    method public void endGroup();
-    method public java.util.List<androidx.compose.runtime.KeyInfo> extractKeys();
-    method public Object? get(int index);
-    method public int getCurrentEnd();
-    method public int getCurrentGroup();
-    method public Object! getGroupAux();
-    method public int getGroupEnd();
-    method public int getGroupKey();
-    method public Object! getGroupNode();
-    method public Object! getGroupObjectKey();
-    method public int getGroupSize();
-    method public int getGroupSlotCount();
-    method public int getGroupSlotIndex();
-    method public boolean getInEmpty();
-    method public int getNodeCount();
-    method public int getParent();
-    method public int getParentNodes();
-    method public int getSize();
-    method public int getSlot();
-    method public Object? groupAux(int index);
-    method public int groupEnd(int index);
-    method public Object? groupGet(int index);
-    method public int groupKey(int index);
-    method public int groupKey(androidx.compose.runtime.Anchor anchor);
-    method public Object? groupObjectKey(int index);
-    method public int groupSize(int index);
-    method public boolean hasObjectKey(int index);
-    method public boolean isGroupEnd();
-    method public boolean isNode();
-    method public boolean isNode(int index);
-    method public Object? next();
-    method public Object? node(int index);
-    method public int nodeCount(int index);
-    method public int parent(int index);
-    method public int parentOf(int index);
-    method public void reposition(int index);
-    method public void restoreParent(int index);
-    method public int skipGroup();
-    method public void skipToGroupEnd();
-    method public void startGroup();
-    method public void startNode();
-    property public final int currentEnd;
-    property public final int currentGroup;
-    property public final Object! groupAux;
-    property public final int groupEnd;
-    property public final int groupKey;
-    property public final Object! groupNode;
-    property public final Object! groupObjectKey;
-    property public final int groupSize;
-    property public final int groupSlotCount;
-    property public final int groupSlotIndex;
-    property public final boolean inEmpty;
-    property public final boolean isGroupEnd;
-    property public final boolean isNode;
-    property public final int nodeCount;
-    property public final int parent;
-    property public final int parentNodes;
-    property public final int size;
-    property public final int slot;
-  }
-
-  @androidx.compose.runtime.InternalComposeApi public final class SlotTable {
-    ctor public SlotTable();
-    method public int anchorIndex(androidx.compose.runtime.Anchor anchor);
-    method public String asString();
-    method public int[] getGroups();
-    method public int getGroupsSize();
-    method public Object![] getSlots();
-    method public int getSlotsSize();
-    method public boolean isEmpty();
-    method public androidx.compose.runtime.SlotReader openReader();
-    method public androidx.compose.runtime.SlotWriter openWriter();
-    method public boolean ownsAnchor(androidx.compose.runtime.Anchor anchor);
-    method public inline <T> T! read(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.SlotReader,? extends T> block);
-    method public void verifyWellFormed();
-    method public inline <T> T! write(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.SlotWriter,? extends T> block);
-    property public final int[] groups;
-    property public final int groupsSize;
-    property public final boolean isEmpty;
-    property public final Object![] slots;
-    property public final int slotsSize;
   }
 
   public final class SlotTableKt {
     method public static java.util.List<java.lang.Integer> slice(int[], Iterable<java.lang.Integer> indices);
   }
 
-  @androidx.compose.runtime.InternalComposeApi public final class SlotWriter {
-    method public void advanceBy(int amount);
-    method public androidx.compose.runtime.Anchor anchor(optional int index);
-    method public int anchorIndex(androidx.compose.runtime.Anchor anchor);
-    method public void beginInsert();
-    method public void close();
-    method public int endGroup();
-    method public void endInsert();
-    method public void ensureStarted(int index);
-    method public void ensureStarted(androidx.compose.runtime.Anchor anchor);
-    method public boolean getClosed();
-    method public int getCurrentGroup();
-    method public int getParent();
-    method public Object? groupAux(int index);
-    method public int groupKey(int index);
-    method public Object? groupObjectKey(int index);
-    method public int groupSize(int index);
-    method public java.util.Iterator<java.lang.Object> groupSlots();
-    method public String groupsAsString();
-    method public boolean isGroupEnd();
-    method public boolean isNode();
-    method public java.util.List<androidx.compose.runtime.Anchor> moveFrom(androidx.compose.runtime.SlotTable table, int index);
-    method public void moveGroup(int offset);
-    method public Object? node(int index);
-    method public int parent(int index);
-    method public int parent(androidx.compose.runtime.Anchor anchor);
-    method public boolean removeGroup();
-    method public void seek(androidx.compose.runtime.Anchor anchor);
-    method public void set(Object? value);
-    method public Object? set(int index, Object? value);
-    method public Object? skip();
-    method public int skipGroup();
-    method public void skipToGroupEnd();
-    method public void startData(int key, Object? objectKey, Object? aux);
-    method public void startData(int key, Object? aux);
-    method public void startGroup();
-    method public void startGroup(int key);
-    method public void startGroup(int key, Object? dataKey);
-    method public void startNode(Object? key);
-    method public void startNode(Object? key, Object? node);
-    method public Object? update(Object? value);
-    method public void updateAux(Object? value);
-    method public void updateNode(Object? value);
-    method public void updateParentNode(Object? value);
-    property public final boolean closed;
-    property public final int currentGroup;
-    property public final boolean isGroupEnd;
-    property public final boolean isNode;
-    property public final int parent;
-  }
-
   public interface SnapshotMutationPolicy<T> {
     method public boolean equivalent(T? a, T? b);
     method @androidx.compose.runtime.ExperimentalComposeApi public default T? merge(T? previous, T? current, T? applied);
@@ -545,16 +391,11 @@
   }
 
   public final class Updater<T> {
-    ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
     method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
     method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
 }
@@ -698,6 +539,7 @@
   }
 
   public final class LiveLiteralKt {
+    method @androidx.compose.runtime.InternalComposeApi public static void enableLiveLiterals();
     method public static boolean isLiveLiteralsEnabled();
     method @androidx.compose.runtime.InternalComposeApi public static <T> androidx.compose.runtime.State<T> liveLiteral(String key, T? value);
     method @androidx.compose.runtime.InternalComposeApi public static void updateLiveLiteralValue(String key, Object? value);
@@ -909,7 +751,7 @@
 package androidx.compose.runtime.tooling {
 
   public final class InspectionTablesKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<java.util.Set<androidx.compose.runtime.SlotTable>> getInspectionTables();
+    method public static androidx.compose.runtime.ProvidableAmbient<java.util.Set<androidx.compose.runtime.CompositionData>> getInspectionTables();
   }
 
 }
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 3d9fadf..f0c99aa 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.compose.runtime {
 
-  @androidx.compose.runtime.ExperimentalComposeApi public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
+  public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
     ctor public AbstractApplier(T? root);
     method public final void clear();
     method public void down(T? node);
@@ -24,8 +24,8 @@
   }
 
   @androidx.compose.runtime.Stable public abstract sealed class Ambient<T> {
-    method public final inline T! getCurrent();
-    property public final inline T! current;
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! getCurrent();
+    property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! current;
   }
 
   public final class AmbientKt {
@@ -34,18 +34,12 @@
     method public static <T> androidx.compose.runtime.ProvidableAmbient<T> staticAmbientOf(optional kotlin.jvm.functions.Function0<? extends T>? defaultFactory);
   }
 
-  @androidx.compose.runtime.InternalComposeApi public final class Anchor {
-    method public boolean getValid();
-    method public int toIndexFor(androidx.compose.runtime.SlotTable slots);
-    method public int toIndexFor(androidx.compose.runtime.SlotWriter writer);
-    property public final boolean valid;
-  }
-
-  @androidx.compose.runtime.ExperimentalComposeApi public interface Applier<N> {
+  public interface Applier<N> {
     method public void clear();
     method public void down(N? node);
     method public N! getCurrent();
-    method public void insert(int index, N? instance);
+    method public void insertBottomUp(int index, N? instance);
+    method public void insertTopDown(int index, N? instance);
     method public void move(int from, int to, int count);
     method public default void onBeginChanges();
     method public default void onEndChanges();
@@ -61,10 +55,10 @@
     method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
   }
 
-  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
   }
 
-  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
+  @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
     method public abstract boolean preventCapture() default false;
     method public abstract boolean readonly() default false;
     method public abstract boolean restartable() default true;
@@ -84,7 +78,7 @@
   }
 
   public final class Composer<N> {
-    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+    ctor public Composer(@kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
     method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
     method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -97,24 +91,22 @@
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(double value);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
     method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
+    method @androidx.compose.runtime.InternalComposeApi public void collectParameterInformation();
     method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method @androidx.compose.runtime.ComposeCompilerApi @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
-    method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
-    method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
+    method @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
+    method @kotlin.PublishedApi internal void emitNode(Object? node);
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
-    method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
+    method @kotlin.PublishedApi internal void endNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
     method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
-    method public androidx.compose.runtime.Applier<N> getApplier();
     method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
+    method public androidx.compose.runtime.CompositionData getCompositionData();
     method public int getCurrentCompoundKeyHash();
     method public boolean getDefaultsInvalid();
-    method public boolean getInserting();
     method public boolean getSkipping();
-    method public androidx.compose.runtime.SlotTable getSlotTable();
     method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
-    method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
+    method @kotlin.PublishedApi internal Object? nextSlot();
     method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
     method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
     method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -124,24 +116,24 @@
     method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
+    method @kotlin.PublishedApi internal void startNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi @kotlin.PublishedApi internal void updateValue(Object? value);
-    method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
-    property public final androidx.compose.runtime.Applier<N> applier;
+    method @kotlin.PublishedApi internal void updateValue(Object? value);
+    method @kotlin.PublishedApi internal N! useNode();
+    method @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
     property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
+    property public final androidx.compose.runtime.CompositionData compositionData;
     property public final int currentCompoundKeyHash;
     property public final boolean defaultsInvalid;
-    property public final boolean inserting;
     property public final boolean skipping;
-    property public final androidx.compose.runtime.SlotTable slotTable;
+    field @kotlin.PublishedApi internal boolean inserting;
   }
 
   public final class ComposerKt {
-    method public static androidx.compose.runtime.Composer<?> getCurrentComposer();
+    method @androidx.compose.runtime.Composable public static androidx.compose.runtime.Composer<?> getCurrentComposer();
     field @kotlin.PublishedApi internal static final androidx.compose.runtime.OpaqueKey ambientMap;
     field @kotlin.PublishedApi internal static final int ambientMapKey = 202; // 0xca
     field @kotlin.PublishedApi internal static final androidx.compose.runtime.OpaqueKey invocation;
@@ -162,6 +154,24 @@
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public interface CompositionData {
+    method public Iterable<androidx.compose.runtime.CompositionGroup> getCompositionGroups();
+    method public boolean isEmpty();
+    property public abstract Iterable<androidx.compose.runtime.CompositionGroup> compositionGroups;
+    property public abstract boolean isEmpty;
+  }
+
+  public interface CompositionGroup extends androidx.compose.runtime.CompositionData {
+    method public Iterable<java.lang.Object> getData();
+    method public Object getKey();
+    method public Object? getNode();
+    method public String? getSourceInfo();
+    property public abstract Iterable<java.lang.Object> data;
+    property public abstract Object key;
+    property public abstract Object? node;
+    property public abstract String? sourceInfo;
+  }
+
   public final class CompositionKt {
     method @androidx.compose.runtime.ExperimentalComposeApi public static androidx.compose.runtime.Composition compositionFor(Object key, androidx.compose.runtime.Applier<?> applier, androidx.compose.runtime.CompositionReference parent, optional kotlin.jvm.functions.Function0<kotlin.Unit> onCreated);
   }
@@ -198,7 +208,7 @@
 
   public final class EffectsKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.CompositionReference compositionReference();
-    method public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
+    method @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
     method @androidx.compose.runtime.Composable public static void onActive(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
     method @androidx.compose.runtime.Composable public static inline void onCommit(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
     method @androidx.compose.runtime.Composable public static <V1> void onCommit(V1? v1, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
@@ -294,19 +304,6 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
   }
 
-  public final class ObserverMap<K, V> {
-    ctor public ObserverMap();
-    method public void add(K key, V value);
-    method public void clear();
-    method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
-    method public boolean contains(K key, V value);
-    method public operator java.util.List<V> get(Iterable<? extends K> keys);
-    method public java.util.List<V> getValueOf(K key);
-    method public void remove(K key);
-    method public void remove(K key, V value);
-    method public void removeValue(V value);
-  }
-
   @kotlin.PublishedApi internal final class PreCommitScopeImpl implements androidx.compose.runtime.CommitScope androidx.compose.runtime.CompositionLifecycleObserver {
     ctor public PreCommitScopeImpl(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> onCommit);
     method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
@@ -385,101 +382,8 @@
   }
 
   public final class SkippableUpdater<T> {
-    ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
+    ctor @kotlin.PublishedApi internal SkippableUpdater(@kotlin.PublishedApi androidx.compose.runtime.Composer<?> composer, @kotlin.PublishedApi T? node);
     method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
-  }
-
-  @androidx.compose.runtime.InternalComposeApi public final class SlotReader {
-    ctor public SlotReader(androidx.compose.runtime.SlotTable table);
-    method public androidx.compose.runtime.Anchor anchor(optional int index);
-    method public void beginEmpty();
-    method public void close();
-    method public void endEmpty();
-    method public void endGroup();
-    method public java.util.List<androidx.compose.runtime.KeyInfo> extractKeys();
-    method public Object? get(int index);
-    method public int getCurrentEnd();
-    method public int getCurrentGroup();
-    method public Object! getGroupAux();
-    method public int getGroupEnd();
-    method public int getGroupKey();
-    method public Object! getGroupNode();
-    method public Object! getGroupObjectKey();
-    method public int getGroupSize();
-    method public int getGroupSlotCount();
-    method public int getGroupSlotIndex();
-    method public boolean getInEmpty();
-    method public int getNodeCount();
-    method public int getParent();
-    method public int getParentNodes();
-    method public int getSize();
-    method public int getSlot();
-    method public Object? groupAux(int index);
-    method public int groupEnd(int index);
-    method public Object? groupGet(int index);
-    method public int groupKey(int index);
-    method public int groupKey(androidx.compose.runtime.Anchor anchor);
-    method public Object? groupObjectKey(int index);
-    method public int groupSize(int index);
-    method public boolean hasObjectKey(int index);
-    method public boolean isGroupEnd();
-    method public boolean isNode();
-    method public boolean isNode(int index);
-    method public Object? next();
-    method public Object? node(int index);
-    method public int nodeCount(int index);
-    method public int parent(int index);
-    method public int parentOf(int index);
-    method public void reposition(int index);
-    method public void restoreParent(int index);
-    method public int skipGroup();
-    method public void skipToGroupEnd();
-    method public void startGroup();
-    method public void startNode();
-    property public final int currentEnd;
-    property public final int currentGroup;
-    property public final Object! groupAux;
-    property public final int groupEnd;
-    property public final int groupKey;
-    property public final Object! groupNode;
-    property public final Object! groupObjectKey;
-    property public final int groupSize;
-    property public final int groupSlotCount;
-    property public final int groupSlotIndex;
-    property public final boolean inEmpty;
-    property public final boolean isGroupEnd;
-    property public final boolean isNode;
-    property public final int nodeCount;
-    property public final int parent;
-    property public final int parentNodes;
-    property public final int size;
-    property public final int slot;
-  }
-
-  @androidx.compose.runtime.InternalComposeApi public final class SlotTable {
-    ctor public SlotTable();
-    method public int anchorIndex(androidx.compose.runtime.Anchor anchor);
-    method public String asString();
-    method public int[] getGroups();
-    method public int getGroupsSize();
-    method public Object![] getSlots();
-    method public int getSlotsSize();
-    method public boolean isEmpty();
-    method public androidx.compose.runtime.SlotReader openReader();
-    method public androidx.compose.runtime.SlotWriter openWriter();
-    method public boolean ownsAnchor(androidx.compose.runtime.Anchor anchor);
-    method public inline <T> T! read(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.SlotReader,? extends T> block);
-    method public void verifyWellFormed();
-    method public inline <T> T! write(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.SlotWriter,? extends T> block);
-    property public final int[] groups;
-    property public final int groupsSize;
-    property public final boolean isEmpty;
-    property public final Object![] slots;
-    property public final int slotsSize;
   }
 
   public final class SlotTableKt {
@@ -487,57 +391,6 @@
     field @kotlin.PublishedApi internal static final Object EMPTY;
   }
 
-  @androidx.compose.runtime.InternalComposeApi public final class SlotWriter {
-    method public void advanceBy(int amount);
-    method public androidx.compose.runtime.Anchor anchor(optional int index);
-    method public int anchorIndex(androidx.compose.runtime.Anchor anchor);
-    method public void beginInsert();
-    method public void close();
-    method public int endGroup();
-    method public void endInsert();
-    method public void ensureStarted(int index);
-    method public void ensureStarted(androidx.compose.runtime.Anchor anchor);
-    method public boolean getClosed();
-    method public int getCurrentGroup();
-    method public int getParent();
-    method public Object? groupAux(int index);
-    method public int groupKey(int index);
-    method public Object? groupObjectKey(int index);
-    method public int groupSize(int index);
-    method public java.util.Iterator<java.lang.Object> groupSlots();
-    method public String groupsAsString();
-    method public boolean isGroupEnd();
-    method public boolean isNode();
-    method public java.util.List<androidx.compose.runtime.Anchor> moveFrom(androidx.compose.runtime.SlotTable table, int index);
-    method public void moveGroup(int offset);
-    method public Object? node(int index);
-    method public int parent(int index);
-    method public int parent(androidx.compose.runtime.Anchor anchor);
-    method public boolean removeGroup();
-    method public void seek(androidx.compose.runtime.Anchor anchor);
-    method public void set(Object? value);
-    method public Object? set(int index, Object? value);
-    method public Object? skip();
-    method public int skipGroup();
-    method public void skipToGroupEnd();
-    method public void startData(int key, Object? objectKey, Object? aux);
-    method public void startData(int key, Object? aux);
-    method public void startGroup();
-    method public void startGroup(int key);
-    method public void startGroup(int key, Object? dataKey);
-    method public void startNode(Object? key);
-    method public void startNode(Object? key, Object? node);
-    method public Object? update(Object? value);
-    method public void updateAux(Object? value);
-    method public void updateNode(Object? value);
-    method public void updateParentNode(Object? value);
-    property public final boolean closed;
-    property public final int currentGroup;
-    property public final boolean isGroupEnd;
-    property public final boolean isNode;
-    property public final int parent;
-  }
-
   public interface SnapshotMutationPolicy<T> {
     method public boolean equivalent(T? a, T? b);
     method @androidx.compose.runtime.ExperimentalComposeApi public default T? merge(T? previous, T? current, T? applied);
@@ -573,16 +426,12 @@
   }
 
   public final class Updater<T> {
-    ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
+    ctor @kotlin.PublishedApi internal Updater(@kotlin.PublishedApi androidx.compose.runtime.Composer<?> composer, @kotlin.PublishedApi T? node);
     method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
     method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
     method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
 }
@@ -728,6 +577,7 @@
   }
 
   public final class LiveLiteralKt {
+    method @androidx.compose.runtime.InternalComposeApi public static void enableLiveLiterals();
     method public static boolean isLiveLiteralsEnabled();
     method @androidx.compose.runtime.InternalComposeApi public static <T> androidx.compose.runtime.State<T> liveLiteral(String key, T? value);
     method @androidx.compose.runtime.InternalComposeApi public static void updateLiveLiteralValue(String key, Object? value);
@@ -949,7 +799,7 @@
 package androidx.compose.runtime.tooling {
 
   public final class InspectionTablesKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<java.util.Set<androidx.compose.runtime.SlotTable>> getInspectionTables();
+    method public static androidx.compose.runtime.ProvidableAmbient<java.util.Set<androidx.compose.runtime.CompositionData>> getInspectionTables();
   }
 
 }
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 5a834fb..ab49bf9 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -51,6 +51,7 @@
         testImplementation KOTLIN_TEST_JUNIT
         testImplementation(JUNIT)
         testImplementation(ROBOLECTRIC)
+        testImplementation(KOTLIN_COROUTINES_TEST)
 
         androidTestImplementation KOTLIN_TEST_JUNIT
         androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
@@ -102,6 +103,7 @@
 
             commonTest.dependencies {
                 implementation kotlin("test-junit")
+                implementation(KOTLIN_COROUTINES_TEST)
             }
             androidAndroidTest.dependencies {
                 implementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
index b044cc9..b6ec566 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
@@ -16,9 +16,6 @@
 
 package androidx.compose.runtime.benchmark
 
-import android.app.Activity
-import android.view.View
-import android.view.ViewGroup
 import androidx.benchmark.junit4.BenchmarkRule
 import androidx.benchmark.junit4.measureRepeated
 import androidx.compose.runtime.Composable
@@ -32,7 +29,6 @@
 import androidx.compose.runtime.snapshots.SnapshotReadObserver
 import androidx.compose.runtime.snapshots.SnapshotWriteObserver
 import androidx.compose.runtime.snapshots.takeMutableSnapshot
-import androidx.compose.ui.platform.AndroidOwner
 import androidx.compose.ui.platform.setContent
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
@@ -189,25 +185,3 @@
         updateModelCb = block
     }
 }
-
-// TODO(chuckj): Consider refacgtoring to use AndroidTestCaseRunner from UI
-// This code is copied from AndroidTestCaseRunner.kt
-private fun findComposeView(activity: Activity): AndroidOwner? {
-    return findComposeView(activity.findViewById(android.R.id.content) as ViewGroup)
-}
-
-private fun findComposeView(view: View): AndroidOwner? {
-    if (view is AndroidOwner) {
-        return view
-    }
-
-    if (view is ViewGroup) {
-        for (i in 0 until view.childCount) {
-            val composeView = findComposeView(view.getChildAt(i))
-            if (composeView != null) {
-                return composeView
-            }
-        }
-    }
-    return null
-}
\ No newline at end of file
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt
index 9adeb79..d932950 100644
--- a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt
+++ b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt
@@ -17,10 +17,9 @@
 @file:OptIn(ExperimentalComposeApi::class)
 package androidx.compose.runtime
 
+import android.view.View
 import android.widget.TextView
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.platform.subcomposeInto
+import androidx.compose.ui.node.UiApplier
 import androidx.compose.ui.viewinterop.emitView
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -497,18 +496,20 @@
     }
 
     @Composable fun deferredSubCompose(block: @Composable () -> Unit): () -> Unit {
-        @OptIn(ExperimentalLayoutNodeApi::class)
-        val container = remember { LayoutNode() }
+        val container = remember { View(activity) }
         val ref = Ref<CompositionReference>()
         narrowInvalidateForReference(ref = ref)
         return {
             @OptIn(ExperimentalComposeApi::class)
             // TODO(b/150390669): Review use of @ComposableContract(tracked = false)
-            subcomposeInto(
+            compositionFor(
                 container,
+                UiApplier(container),
                 ref.value
-            ) @ComposableContract(tracked = false) {
-                block()
+            ).apply {
+                setContent @ComposableContract(tracked = false) {
+                    block()
+                }
             }
         }
     }
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
index 0f74688..51a66a2 100644
--- a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
+++ b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
@@ -21,14 +21,13 @@
 import android.os.Bundle
 import android.os.Looper
 import android.view.Choreographer
+import android.view.View
 import android.view.ViewGroup
 import android.widget.LinearLayout
 import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.UiApplier
 import androidx.compose.ui.platform.AmbientContext
 import androidx.compose.ui.platform.setViewContent
-import androidx.compose.ui.platform.subcomposeInto
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlin.test.assertTrue
@@ -117,16 +116,18 @@
 
     @Composable
     fun subCompose(block: @Composable () -> Unit) {
-        @OptIn(ExperimentalLayoutNodeApi::class)
-        val container = remember { LayoutNode() }
+        val container = remember { View(activity) }
         val reference = compositionReference()
         // TODO(b/150390669): Review use of @ComposableContract(tracked = false)
         @OptIn(ExperimentalComposeApi::class)
-        subcomposeInto(
+        compositionFor(
             container,
+            UiApplier(container),
             reference
-        ) @ComposableContract(tracked = false) {
-            block()
+        ).apply {
+            setContent @ComposableContract(tracked = false) {
+                block()
+            }
         }
     }
 }
diff --git a/compose/runtime/runtime/lint-baseline.xml b/compose/runtime/runtime/lint-baseline.xml
deleted file mode 100644
index 76c954a..0000000
--- a/compose/runtime/runtime/lint-baseline.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
-    <issue
-        id="UnknownNullness"
-        message="Should explicitly declare type here since implicit type does not specify nullness"
-        errorLine1="    override fun removeAt(index: Int) = get(index).also { update { it.removeAt(index) } }"
-        errorLine2="                 ~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt"
-            line="91"
-            column="18"/>
-    </issue>
-
-    <issue
-        id="UnknownNullness"
-        message="Should explicitly declare type here since implicit type does not specify nullness"
-        errorLine1="    override fun set(index: Int, element: T) = get(index).also { update { it.set(index, element) } }"
-        errorLine2="                 ~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt"
-            line="93"
-            column="18"/>
-    </issue>
-
-</issues>
diff --git a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt
index 8eed09e..2d29baf 100644
--- a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt
+++ b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt
@@ -41,10 +41,14 @@
     // We would implement an Applier class like the following, which would teach compose how to
     // manage a tree of Nodes.
     class NodeApplier(root: Node) : AbstractApplier<Node>(root) {
-        override fun insert(index: Int, instance: Node) {
+        override fun insertTopDown(index: Int, instance: Node) {
             current.children.add(index, instance)
         }
 
+        override fun insertBottomUp(index: Int, instance: Node) {
+            // Ignored as the tree is built top-down.
+        }
+
         override fun remove(index: Int, count: Int) {
             current.children.remove(index, count)
         }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt
index 9bfa638..d01c5d3 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt
@@ -69,9 +69,10 @@
      * @sample androidx.compose.runtime.samples.consumeAmbient
      */
     @OptIn(ComposeCompilerApi::class)
-    @ComposableContract(readonly = true)
-    @Composable
-    inline val current: T get() = currentComposer.consume(this)
+    inline val current: T
+        @ComposableContract(readonly = true)
+        @Composable
+        get() = currentComposer.consume(this)
 }
 
 /**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index 1f9410b..2d1be12 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -30,7 +30,6 @@
  * @see Composer
  * @see emit
  */
-@ExperimentalComposeApi
 interface Applier<N> {
     /**
      * The node that operations will be applied on at any given time. It is expected that the
@@ -65,9 +64,98 @@
     fun up()
 
     /**
-     * Indicates that [instance] should be inserted as a child to [current] at [index]
+     * Indicates that [instance] should be inserted as a child to [current] at [index]. An applier
+     * should insert the node into the tree either in [insertTopDown] or [insertBottomUp], not both.
+     *
+     * The [insertTopDown] method is called before the children of [instance] have been created and
+     * inserted into it. [insertBottomUp] is called after all children have been created and
+     * inserted.
+     *
+     * Some trees are faster to build top-down, in which case the [insertTopDown] method should
+     * be used to insert the [instance]. Other tress are faster to build bottom-up in which case
+     * [insertBottomUp] should be used.
+     *
+     * To give example of building a tree top-down vs. bottom-up consider the following tree,
+     *
+     * ```
+     *      R
+     *      |
+     *      B
+     *     / \
+     *    A   C
+     *  ```
+     *
+     *  where the node `B` is being inserted into the tree at `R`. Top-down building of the tree
+     *  first inserts `B` into `R`, then inserts `A` into `B` followed by inserting `C` into B`.
+     *  For example,
+     *
+     *  ```
+     *      1           2           3
+     *      R           R           R
+     *      |           |           |
+     *      B           B           B
+     *                 /           / \
+     *                A           A   C
+     * ```
+     *
+     * A bottom-up building of the tree starts with inserting `A` and `C` into `B` then inserts
+     * `B` tree into `R`.
+     *
+     * ```
+     *    1           2           3
+     *    B           B           R
+     *    |          / \          |
+     *    A         A   C         B
+     *                           / \
+     *                          A   C
+     * ```
+     *
+     * To see how building top-down vs. bottom-up can differ significantly in performance
+     * consider a tree where whenever a child is added to the tree all parent nodes, up to the root,
+     * are notified of the new child entering the tree. If the tree is built top-down,
+     *
+     *  1. `R` is notified of `B` entering.
+     *  2. `B` is notified of `A` entering, `R` is notified of `A` entering.
+     *  3. `B` is notified of `C` entering, `R` is notified of `C` entering.
+     *
+     *  for a total of 5 notifications. The number of notifications grows exponentially with the
+     *  number of inserts.
+     *
+     *  For bottom-up, the notifications are,
+     *
+     *  1. `B` is notified `A` entering.
+     *  2. `B` is notified `C` entering.
+     *  3. `R` is notified `B` entering.
+     *
+     *  The notifications are linear to the number of nodes inserted.
+     *
+     *  If, on the other hand, all children are notified when the parent enters a tree, then the
+     *  notifications are, for top-down,
+     *
+     *  1. `B` is notified it is entering `R`.
+     *  2. `A` is notified it is entering `B`.
+     *  3. `C` is notified it is entering `B`.
+     *
+     *  which is linear to the number of nodes inserted.
+     *
+     *  For bottom-up, the notifications look like,
+     *
+     *  1. `A` is notified it is entering `B`.
+     *  2. `C` is notified it is entering `B`.
+     *  3. `B` is notified it is entering `R`, `A` is notified it is entering `R`,
+     *     `C` is notified it is entering `R`.
+     *
+     *  which exponential to the number of nodes inserted.
      */
-    fun insert(index: Int, instance: N)
+    fun insertTopDown(index: Int, instance: N)
+
+    /**
+     * Indicates that [instance] should be inserted as a child of [current] at [index]. An applier
+     * should insert the node into the tree either in [insertTopDown] or [insertBottomUp], not
+     * both. See the description of [insertTopDown] to which describes when to implement
+     * [insertTopDown] and when to use [insertBottomUp].
+     */
+    fun insertBottomUp(index: Int, instance: N)
 
     /**
      * Indicates that the children of [current] from [index] to [index] + [count] should be removed.
@@ -75,10 +163,9 @@
     fun remove(index: Int, count: Int)
 
     /**
-     * Indicates that the children of [current] from [from] to [from] + [count] should be moved
-     * to [to] + [count].
+     * Indicates that [count] children of [current] should be moved from index [from] to index [to].
      *
-     * The [to] index is related to the position before the change, so, for example, to move an
+     * The [to] index is relative to the position before the change, so, for example, to move an
      * element at position 1 to after the element at position 2, [from] should be `1` and [to]
      * should be `3`. If the elements were A B C D E, calling `move(1, 3, 1)` would result in the
      * elements being reordered to A C B D E.
@@ -93,7 +180,7 @@
 }
 
 /**
- * An abstract [Applier] implementation that builds the tree "top down".
+ * An abstract [Applier] implementation.
  *
  * @sample androidx.compose.runtime.samples.CustomTreeComposition
  *
@@ -102,7 +189,6 @@
  * @see Composer
  * @see emit
  */
-@ExperimentalComposeApi
 abstract class AbstractApplier<T>(val root: T) : Applier<T> {
     private val stack = mutableListOf<T>()
     override var current: T = root
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt
index de94b47..f2375f85 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt
@@ -50,8 +50,14 @@
     // foo: (@Composable () -> Unit) -> Unit
     AnnotationTarget.TYPE_PARAMETER,
 
-    // composable property declarations
+    // (DEPRECATED) composable property declarations
     // @Composable val foo: Int get() { ... }
-    AnnotationTarget.PROPERTY
+    AnnotationTarget.PROPERTY,
+
+    // composable property getters and setters
+    // val foo: Int @Composable get() { ... }
+    // var bar: Int
+    //   @Composable get() { ... }
+    AnnotationTarget.PROPERTY_GETTER
 )
 annotation class Composable
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt
index 4a820a8..213e507 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt
@@ -45,7 +45,8 @@
 @Retention(AnnotationRetention.BINARY)
 @Target(
     AnnotationTarget.FUNCTION,
-    AnnotationTarget.PROPERTY,
+    AnnotationTarget.PROPERTY, // (DEPRECATED)
+    AnnotationTarget.PROPERTY_GETTER,
     AnnotationTarget.TYPE
 )
 annotation class ComposableContract(
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 563fabe0..3cc12dd 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 = 1900
+    const val version: Int = 2000
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index b3ea2bc..f6d3736 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -61,7 +61,6 @@
  * changed. It is used to determine how to update the nodes and the slot table when changes to the
  * structure of the tree is detected.
  */
-@OptIn(InternalComposeApi::class)
 private class Pending(
     val keyInfos: MutableList<KeyInfo>,
     val startIndex: Int
@@ -78,7 +77,9 @@
         val result = hashMapOf<Int, GroupInfo>()
         for (index in 0 until keyInfos.size) {
             val keyInfo = keyInfos[index]
+            @OptIn(InternalComposeApi::class)
             result[keyInfo.location] = GroupInfo(index, runningNodeIndex, keyInfo.nodes)
+            @OptIn(InternalComposeApi::class)
             runningNodeIndex += keyInfo.nodes
         }
         result
@@ -147,6 +148,7 @@
         }
     }
 
+    @OptIn(InternalComposeApi::class)
     fun registerInsert(keyInfo: KeyInfo, insertIndex: Int) {
         groupInfos[keyInfo.location] = GroupInfo(-1, insertIndex, 0)
     }
@@ -168,8 +170,13 @@
         return false
     }
 
+    @OptIn(InternalComposeApi::class)
     fun slotPositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.location]?.slotIndex ?: -1
+
+    @OptIn(InternalComposeApi::class)
     fun nodePositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.location]?.nodeIndex ?: -1
+
+    @OptIn(InternalComposeApi::class)
     fun updatedNodeCountOf(keyInfo: KeyInfo) =
         groupInfos[keyInfo.location]?.nodeCount ?: keyInfo.nodes
 }
@@ -344,21 +351,16 @@
  */
 class Composer<N>(
     /**
-     * Backing storage for the composition
-     */
-    val slotTable: SlotTable,
-
-    /**
      * An adapter that applies changes to the tree using the Applier abstraction.
      */
-    @ComposeCompilerApi
-    val applier: Applier<N>,
+    @PublishedApi internal val applier: Applier<N>,
 
     /**
      * Parent of this composition; a [Recomposer] for root-level compositions.
      */
     private val parentReference: CompositionReference
 ) {
+    private val slotTable: SlotTable = SlotTable()
     private val changes = mutableListOf<Change<N>>()
     private val lifecycleObservers = HashMap<
         CompositionLifecycleObserverHolder,
@@ -373,7 +375,7 @@
     private var nodeCountOverrides: IntArray? = null
     private var nodeCountVirtualOverrides: HashMap<Int, Int>? = null
     private var collectKeySources = false
-
+    private var collectParameterInformation = false
     private var nodeExpected = false
     private val observations: MutableList<Any> = mutableListOf()
     private val observationsProcessed: MutableList<Any> = mutableListOf()
@@ -395,7 +397,6 @@
 
     private var reader: SlotReader = slotTable.openReader().also { it.close() }
 
-    @OptIn(InternalComposeApi::class)
     internal val insertTable = SlotTable()
 
     private var writer: SlotWriter = insertTable.openWriter().also { it.close() }
@@ -538,7 +539,7 @@
      * Start the composition. This should be called, and only be called, as the first group in
      * the composition.
      */
-    @OptIn(ComposeCompilerApi::class, InternalComposeApi::class)
+    @OptIn(InternalComposeApi::class)
     private fun startRoot() {
         reader = slotTable.openReader()
         startGroup(rootKey)
@@ -549,6 +550,7 @@
         providersInvalidStack.push(providersInvalid.asInt())
         providersInvalid = changed(parentProvider)
         collectKeySources = parentReference.collectingKeySources
+        collectParameterInformation = parentReference.collectingParameterInformation
         resolveAmbient(InspectionTables, parentProvider)?.let {
             it.add(slotTable)
             parentReference.recordInspectionTable(it)
@@ -560,7 +562,6 @@
      * End the composition. This should be called, and only be called, to end the first group in
      * the composition.
      */
-    @ComposeCompilerApi
     @OptIn(InternalComposeApi::class)
     private fun endRoot() {
         endGroup()
@@ -596,8 +597,8 @@
      * first composition this is always true. During recomposition this is true when new nodes
      * are being scheduled to be added to the tree.
      */
-    @ComposeCompilerApi
-    var inserting: Boolean = false
+    @PublishedApi
+    internal var inserting: Boolean = false
         private set
 
     /**
@@ -628,6 +629,15 @@
     }
 
     /**
+     * Start collecting parameter information. This enables the tools API to always be able to
+     * determine the parameter values of composable calls.
+     */
+    @InternalComposeApi
+    fun collectParameterInformation() {
+        collectParameterInformation = true
+    }
+
+    /**
      * Record that [value] was read from. If [recordWriteOf] or [recordModificationsOf] is called
      * with [value] then the corresponding [currentRecomposeScope] is invalidated.
      *
@@ -661,6 +671,31 @@
     }
 
     /**
+     * Throw a diagnostic exception if the internal tracking tables are inconsistent.
+     */
+    @InternalComposeApi
+    fun verifyConsistent() {
+        if (!isComposing) {
+            slotTable.verifyWellFormed()
+            insertTable.verifyWellFormed()
+            validateRecomposeScopeAnchors(slotTable)
+        }
+    }
+
+    private fun validateRecomposeScopeAnchors(slotTable: SlotTable) {
+        val scopes = slotTable.slots.map { it as? RecomposeScope }.filterNotNull()
+        for (scope in scopes) {
+            scope.anchor?.let { anchor ->
+                check(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
+                    val dataIndex = slotTable.slots.indexOf(scope)
+                    "Misaligned anchor $anchor in scope $scope encountered, scope found at " +
+                        "$dataIndex"
+                }
+            }
+        }
+    }
+
+    /**
      * Record that the objects in [values] have been modified. This invalidates any recomposes
      * scopes  that were current when [recordReadOf] was called with an instance in [values].
      *
@@ -763,7 +798,7 @@
      * Apply the changes to the tree that were collected during the last composition.
      */
     @InternalComposeApi
-    @OptIn(ComposeCompilerApi::class)
+    @OptIn(ExperimentalComposeApi::class)
     fun applyChanges() {
         trace("Compose:applyChanges") {
             invalidateStack.clear()
@@ -812,7 +847,7 @@
     }
 
     @ExperimentalComposeApi
-    @OptIn(ComposeCompilerApi::class, InternalComposeApi::class)
+    @OptIn(InternalComposeApi::class)
     internal fun dispose() {
         trace("Compose:Composer.dispose") {
             parentReference.unregisterComposer(this)
@@ -845,16 +880,13 @@
      *
      *  @param key The key for the group
      */
-    @ComposeCompilerApi
     internal fun startGroup(key: Int) = start(key, null, false, null)
 
-    @ComposeCompilerApi
     internal fun startGroup(key: Int, dataKey: Any?) = start(key, dataKey, false, null)
 
     /**
      * End the current group.
      */
-    @ComposeCompilerApi
     internal fun endGroup() = end(false)
 
     @OptIn(InternalComposeApi::class)
@@ -869,8 +901,8 @@
      * current position if found, if no such node is found the composition switches into insert
      * mode and a the node is scheduled to be inserted at the current location.
      */
-    @ComposeCompilerApi
-    fun startNode() {
+    @PublishedApi
+    internal fun startNode() {
         start(nodeKey, null, true, null)
         nodeExpected = true
     }
@@ -880,9 +912,8 @@
      * call when the composer is inserting.
      */
     @Suppress("UNUSED")
-    @ComposeCompilerApi
-    @OptIn(ExperimentalComposeApi::class, InternalComposeApi::class)
-    fun <T : N> createNode(factory: () -> T) {
+    @OptIn(ExperimentalComposeApi::class)
+    internal fun <T : N> createNode(factory: () -> T) {
         validateNodeExpected()
         check(inserting) { "createNode() can only be called when inserting" }
         val insertIndex = nodeIndexStack.peek()
@@ -891,18 +922,23 @@
         recordFixup { applier, slots, _ ->
             val node = factory()
             slots.node = node
-            applier.insert(insertIndex, node)
+            applier.insertTopDown(insertIndex, node)
             applier.down(node)
         }
+        recordInsertUp { applier, _, _ ->
+            val nodeToInsert = applier.current
+            applier.up()
+            applier.insertBottomUp(insertIndex, nodeToInsert)
+        }
     }
 
     /**
      * Schedule the given node to be inserted. This is only valid to call when the composer is
      * inserting.
      */
-    @ComposeCompilerApi
+    @PublishedApi
     @OptIn(ExperimentalComposeApi::class)
-    fun emitNode(node: Any?) {
+    internal fun emitNode(node: Any?) {
         validateNodeExpected()
         check(inserting) { "emitNode() called when not inserting" }
         val insertIndex = nodeIndexStack.peek()
@@ -911,9 +947,14 @@
         @Suppress("UNCHECKED_CAST")
         writer.node = node as N
         recordApplierOperation { applier, _, _ ->
-            applier.insert(insertIndex, node)
+            applier.insertTopDown(insertIndex, node)
             applier.down(node)
         }
+        recordInsertUp { applier, _, _ ->
+            val nodeToInsert = applier.current
+            applier.up()
+            applier.insertBottomUp(insertIndex, nodeToInsert)
+        }
     }
 
     /**
@@ -921,8 +962,9 @@
      * valid to call when the composition is not inserting. This must be called at the same
      * location as [emitNode] or [createNode] as called even if the value is unused.
      */
-    @ComposeCompilerApi
-    fun useNode(): N {
+    @PublishedApi
+    @OptIn(InternalComposeApi::class)
+    internal fun useNode(): N {
         validateNodeExpected()
         check(!inserting) { "useNode() called while inserting" }
         val result = reader.node
@@ -933,8 +975,8 @@
     /**
      * Called to end the node group.
      */
-    @ComposeCompilerApi
-    fun endNode() = end(true)
+    @PublishedApi
+    internal fun endNode() = end(true)
 
     /**
      * Schedule a change to be applied to a node's property. This change will be applied to the
@@ -961,9 +1003,9 @@
     /**
      * Return the next value in the slot table and advance the current location.
      */
-    @ComposeCompilerApi
+    @PublishedApi
     @OptIn(InternalComposeApi::class)
-    fun nextSlot(): Any? = if (inserting) {
+    internal fun nextSlot(): Any? = if (inserting) {
         validateNodeNotExpected()
         EMPTY
     } else reader.next()
@@ -1099,9 +1141,8 @@
      *
      * @param value the value to schedule to be written to the slot table.
      */
-    @OptIn(InternalComposeApi::class)
     @PublishedApi
-    @ComposeCompilerApi
+    @OptIn(InternalComposeApi::class)
     internal fun updateValue(value: Any?) {
         if (inserting) {
             writer.update(value)
@@ -1127,6 +1168,9 @@
         }
     }
 
+    @InternalComposeApi
+    val compositionData: CompositionData get() = slotTable
+
     /**
      * Schedule a side effect to run when we apply composition changes.
      */
@@ -1137,8 +1181,6 @@
     /**
      * Return the current ambient scope which was provided by a parent group.
      */
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun currentAmbientScope(): AmbientMap {
         if (inserting && hasProvider) {
             var current = writer.parent
@@ -1173,8 +1215,6 @@
      * compose which might be inserting the sub-composition. In that case the current scope
      * is the correct scope.
      */
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun ambientScopeAt(location: Int): AmbientMap {
         if (isComposing) {
             // The sub-composer is being composed as part of a nested composition then use the
@@ -1204,7 +1244,6 @@
      * scope followed by the map used to augment the parent scope. Both are needed to detect
      * inserts, updates and deletes to the providers.
      */
-    @ComposeCompilerApi
     private fun updateProviderMapGroup(
         parentScope: AmbientMap,
         currentProviders: AmbientMap
@@ -1217,8 +1256,6 @@
         return providerScope
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     internal fun startProviders(values: Array<out ProvidedValue<*>>) {
         val parentScope = currentAmbientScope()
         startGroup(providerKey, provider)
@@ -1268,31 +1305,32 @@
         start(ambientMapKey, ambientMap, false, providers)
     }
 
-    @ComposeCompilerApi
     internal fun endProviders() {
         endGroup()
         endGroup()
         providersInvalid = providersInvalidStack.pop().asBool()
     }
 
-    @ComposeCompilerApi
     @PublishedApi
     internal fun <T> consume(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
 
     /**
      * Create or use a memoized `CompositionReference` instance at this position in the slot table.
      */
-    @ComposeCompilerApi
-    @OptIn(ExperimentalComposeApi::class)
     internal fun buildReference(): CompositionReference {
         startGroup(referenceKey, reference)
 
         var ref = nextSlot() as? CompositionReferenceHolder<*>
-        if (ref == null || !inserting) {
+        if (ref == null) {
             val scope = invalidateStack.peek()
             scope.used = true
             ref = CompositionReferenceHolder(
-                CompositionReferenceImpl(scope, currentCompoundKeyHash, collectKeySources)
+                CompositionReferenceImpl(
+                    scope,
+                    currentCompoundKeyHash,
+                    collectKeySources,
+                    collectParameterInformation
+                )
             )
             updateValue(ref)
         }
@@ -1304,10 +1342,8 @@
     private fun <T> resolveAmbient(key: Ambient<T>, scope: AmbientMap): T =
         if (scope.contains(key)) scope.getValueOf(key) else parentReference.getAmbient(key)
 
-    @ComposeCompilerApi
     internal fun <T> parentAmbient(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
 
-    @ComposeCompilerApi
     private fun <T> parentAmbient(key: Ambient<T>, location: Int): T =
         resolveAmbient(key, ambientScopeAt(location))
 
@@ -1324,7 +1360,6 @@
             if (childrenComposing == 0 && it.isNotEmpty()) it.peek() else null
         }
 
-    @OptIn(InternalComposeApi::class)
     private fun ensureWriter() {
         if (writer.closed) {
             writer = insertTable.openWriter()
@@ -1337,7 +1372,6 @@
     /**
      * Start the reader group updating the data of the group if necessary
      */
-    @OptIn(InternalComposeApi::class)
     private fun startReaderGroup(isNode: Boolean, data: Any?) {
         if (isNode) {
             reader.startNode()
@@ -1351,8 +1385,6 @@
         }
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {
         validateNodeNotExpected()
 
@@ -1478,7 +1510,6 @@
         groupNodeCount = 0
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun exitGroup(expectedNodeCount: Int, inserting: Boolean) {
         // Restore the parent's state updating them if they have changed based on changes in the
         // children. For example, if a group generates nodes then the number of generated nodes will
@@ -1493,8 +1524,6 @@
         this.groupNodeCount = this.groupNodeCountStack.pop() + expectedNodeCount
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     private fun end(isNode: Boolean) {
         // All the changes to the group (or node) have been recorded. All new nodes have been
         // inserted but it has yet to determine which need to be removed or moved. Note that the
@@ -1608,7 +1637,7 @@
         val inserting = inserting
         if (inserting) {
             if (isNode) {
-                recordInsertUp()
+                registerInsertUp()
                 expectedNodeCount = 1
             }
             reader.endEmpty()
@@ -1629,8 +1658,8 @@
             if (isNode) recordUp()
             recordEndGroup()
             val parentGroup = reader.parent
-            val parentNodecount = updatedNodeCount(parentGroup)
-            if (expectedNodeCount != parentNodecount) {
+            val parentNodeCount = updatedNodeCount(parentGroup)
+            if (expectedNodeCount != parentNodeCount) {
                 updateNodeCountOverrides(parentGroup, expectedNodeCount)
             }
             if (isNode) {
@@ -1649,7 +1678,6 @@
      * called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children
      * are invalid it will call [skipReaderToGroupEnd].
      */
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     private fun recomposeToGroupEnd() {
         val wasComposing = isComposing
         isComposing = true
@@ -1738,7 +1766,6 @@
      * updates that count and then updates any parent groups that include the nodes this group
      * emits.
      */
-    @OptIn(InternalComposeApi::class)
     private fun updateNodeCountOverrides(group: Int, newCount: Int) {
         // The value of group can be negative which indicates it is tracking an inserted group
         // instead of an existing group. The index is a virtual index calculated by
@@ -1778,7 +1805,6 @@
      * [recomposeIndex] allows the calculation to exit early if there is no node group between
      * [group] and [recomposeGroup].
      */
-    @OptIn(InternalComposeApi::class)
     private fun nodeIndexOf(
         groupLocation: Int,
         group: Int,
@@ -1814,7 +1840,6 @@
         return index
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun updatedNodeCount(group: Int): Int {
         if (group < 0) return nodeCountVirtualOverrides?.let { it[group] } ?: 0
         val nodeCounts = nodeCountOverrides
@@ -1825,7 +1850,6 @@
         return reader.nodeCount(group)
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun updateNodeCount(group: Int, count: Int) {
         if (updatedNodeCount(group) != count) {
             if (group < 0) {
@@ -1856,7 +1880,6 @@
      * Records the operations necessary to move the applier the node affected by the previous
      * group to the new group.
      */
-    @OptIn(InternalComposeApi::class)
     private fun recordUpsAndDowns(oldGroup: Int, newGroup: Int, commonRoot: Int) {
         val reader = reader
         val nearestCommonRoot = reader.nearestCommonRootOf(
@@ -1876,7 +1899,6 @@
         doRecordDownsFor(newGroup, nearestCommonRoot)
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun doRecordDownsFor(group: Int, nearestCommonRoot: Int) {
         if (group > 0 && group != nearestCommonRoot) {
             doRecordDownsFor(reader.parent(group), nearestCommonRoot)
@@ -1889,7 +1911,6 @@
      * for [group]. Passing in the [recomposeGroup] and [recomposeKey] allows this method to exit
      * early.
      */
-    @OptIn(InternalComposeApi::class)
     private fun compoundKeyOf(group: Int, recomposeGroup: Int, recomposeKey: Int): Int {
         return if (group == recomposeGroup) recomposeKey else (
             compoundKeyOf(
@@ -1909,12 +1930,10 @@
      * back into a known good state after a period of time when snapshot changes were not
      * being observed.
      */
-    @OptIn(InternalComposeApi::class)
     internal fun invalidateAll() {
         slotTable.slots.forEach { (it as? RecomposeScope)?.invalidate() }
     }
 
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     internal fun invalidate(scope: RecomposeScope): InvalidationResult {
         if (scope.defaultsInScope) {
             scope.defaultsInvalid = true
@@ -1940,7 +1959,6 @@
      * composition is not inserting.
      */
     @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     fun skipCurrentGroup() {
         if (invalidations.isEmpty()) {
             skipGroup()
@@ -1956,7 +1974,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun skipReaderToGroupEnd() {
         groupNodeCount = reader.parentNodes
         reader.skipToGroupEnd()
@@ -1994,8 +2011,6 @@
         addRecomposeScope()
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun addRecomposeScope() {
         if (inserting) {
             val scope = RecomposeScope(this)
@@ -2016,14 +2031,13 @@
      * [endRestartGroup]).
      */
     @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     fun endRestartGroup(): ScopeUpdateScope? {
         // This allows for the invalidate stack to be out of sync since this might be called during exception stack
         // unwinding that might have not called the doneJoin/endRestartGroup in the wrong order.
         val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
         else null
         scope?.requiresRecompose = false
-        val result = if (scope != null && (scope.used || collectKeySources)) {
+        val result = if (scope != null && (scope.used || collectParameterInformation)) {
             if (scope.anchor == null) {
                 scope.anchor = if (inserting) {
                     writer.anchor(writer.parent)
@@ -2045,7 +2059,6 @@
      * which must be applied by [applyChanges] to build the tree implied by [block].
      */
     @InternalComposeApi
-    @OptIn(ComposeCompilerApi::class)
     fun composeInitial(block: @Composable () -> Unit) {
         trace("Compose:recompose") {
             var complete = false
@@ -2070,7 +2083,6 @@
      * applied by [applyChanges] to have an effect.
      */
     @InternalComposeApi
-    @OptIn(ComposeCompilerApi::class)
     fun recompose(): Boolean {
         if (invalidations.isNotEmpty()) {
             trace("Compose:recompose") {
@@ -2095,15 +2107,15 @@
 
     internal fun hasInvalidations() = invalidations.isNotEmpty()
 
-    @OptIn(InternalComposeApi::class)
     @Suppress("UNCHECKED_CAST")
+    @OptIn(InternalComposeApi::class)
     private var SlotWriter.node
         get() = node(currentGroup) as N
         set(value) { updateParentNode(value) }
-    @OptIn(InternalComposeApi::class)
+
     @Suppress("UNCHECKED_CAST")
     private val SlotReader.node get() = node(parent) as N
-    @OptIn(InternalComposeApi::class)
+
     @Suppress("UNCHECKED_CAST")
     private fun SlotReader.nodeAt(index: Int) = node(index) as N
 
@@ -2169,7 +2181,6 @@
     private var pendingUps = 0
     private var downNodes = Stack<N>()
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeUps() {
         val count = pendingUps
         if (count > 0) {
@@ -2178,7 +2189,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeDowns(nodes: Array<N>) {
         record { applier, _, _ ->
             for (index in nodes.indices) {
@@ -2208,21 +2218,32 @@
         }
     }
 
-    private var pendingInsertUps = 0
+    private var insertUpRequests = Stack<Change<N>>()
 
-    private fun recordInsertUp() {
-        pendingInsertUps++
+    private var pendingInsertUps = mutableListOf<Change<N>>()
+
+    private fun registerInsertUp() {
+        pendingInsertUps.add(insertUpRequests.pop())
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeInsertUps() {
-        if (pendingInsertUps > 0) {
-            val count = pendingInsertUps
-            record { applier, _, _ -> repeat(count) { applier.up() } }
-            pendingInsertUps = 0
+        if (pendingInsertUps.isNotEmpty()) {
+            pendingInsertUps.forEach { record(it) }
+            pendingInsertUps.clear()
         }
     }
 
+    /**
+     * Record a change that will be added after all the changes for the node and all its children
+     * have been performed.
+     *
+     * This is used to implement calling [Applier.insertBottomUp] to allow a tree to be built
+     * bottom-up instead of top-down.
+     */
+    private fun recordInsertUp(change: Change<N>) {
+        insertUpRequests.push(change)
+    }
+
     // Navigating the writer slot is performed relatively as the location of a group in the writer
     // might be different than it is in the reader as groups can be inserted, deleted, or moved.
     //
@@ -2259,7 +2280,6 @@
      */
     private val startedGroups = IntStack()
 
-    @OptIn(InternalComposeApi::class)
     private fun realizeOperationLocation(forParent: Boolean = false) {
         val location = if (forParent) reader.parent else reader.currentGroup
         val distance = location - writersReaderDelta
@@ -2270,7 +2290,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordInsert(anchor: Anchor) {
         if (insertFixups.isEmpty()) {
             recordSlotEditingOperation { _, slots, _ ->
@@ -2294,7 +2313,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordFixup(change: Change<N>) {
         realizeInsertUps()
         val anchor = insertAnchor
@@ -2312,7 +2330,6 @@
      * writer and reader are tracking the same slot we advance the [writersReaderDelta] to
      * account for the removal.
      */
-    @OptIn(InternalComposeApi::class)
     private fun recordDelete() {
         recordSlotEditingOperation(change = removeCurrentGroupInstance)
         writersReaderDelta += reader.groupSize
@@ -2321,7 +2338,6 @@
     /**
      * Called when reader current is moved directly, such as when a group moves, to [location].
      */
-    @OptIn(InternalComposeApi::class)
     private fun recordReaderMoving(location: Int) {
         val distance = reader.currentGroup - writersReaderDelta
 
@@ -2329,7 +2345,6 @@
         writersReaderDelta = location - distance
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordSlotEditing() {
         // During initial composition (when the slot table is empty), no group needs
         // to be started.
@@ -2350,7 +2365,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordEndGroup() {
         val location = reader.parent
         val currentStartedGroup = startedGroups.peekOr(-1)
@@ -2376,7 +2390,6 @@
         cleanUpCompose()
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun cleanUpCompose() {
         pending = null
         nodeIndex = 0
@@ -2421,7 +2434,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeMovement() {
         val count = previousCount
         previousCount = 0
@@ -2457,9 +2469,10 @@
     private inner class CompositionReferenceImpl(
         val scope: RecomposeScope,
         override val compoundHashKey: Int,
-        override val collectingKeySources: Boolean
+        override val collectingKeySources: Boolean,
+        override val collectingParameterInformation: Boolean
     ) : CompositionReference() {
-        var inspectionTables: MutableSet<MutableSet<SlotTable>>? = null
+        var inspectionTables: MutableSet<MutableSet<CompositionData>>? = null
         val composers = mutableSetOf<Composer<*>>()
 
         fun dispose() {
@@ -2501,11 +2514,19 @@
         }
 
         override fun invalidate(composer: Composer<*>) {
-            invalidate(scope)
+            // Invalidate ourselves with our parent before we invalidate a child composer.
+            // This ensures that when we are scheduling recompositions, parents always
+            // recompose before their children just in case a recomposition in the parent
+            // would also cause other recomposition in the child.
+            // If the parent ends up having no real invalidations to process we will skip work
+            // for that composer along a fast path later.
+            // This invalidation process could be made more efficient as it's currently N^2 with
+            // subcomposition meta-tree depth thanks to the double recursive parent walk
+            // performed here, but we currently assume a low N.
+            parentReference.invalidate(this@Composer)
+            parentReference.invalidate(composer)
         }
 
-        @ComposeCompilerApi
-        @OptIn(InternalComposeApi::class)
         override fun <T> getAmbient(key: Ambient<T>): T {
             val anchor = scope.anchor
             return if (anchor != null && anchor.valid) {
@@ -2517,15 +2538,13 @@
             }
         }
 
-        @ComposeCompilerApi
-        @OptIn(InternalComposeApi::class)
         override fun getAmbientScope(): AmbientMap {
             return ambientScopeAt(scope.anchor?.toIndexFor(slotTable) ?: 0)
         }
 
-        override fun recordInspectionTable(table: MutableSet<SlotTable>) {
+        override fun recordInspectionTable(table: MutableSet<CompositionData>) {
             (
-                inspectionTables ?: HashSet<MutableSet<SlotTable>>().also {
+                inspectionTables ?: HashSet<MutableSet<CompositionData>>().also {
                     inspectionTables = it
                 }
                 ).add(table)
@@ -2540,7 +2559,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun updateCompoundKeyWhenWeEnterGroup(groupKey: Int, dataKey: Any?) {
         if (dataKey == null)
             updateCompoundKeyWhenWeEnterGroupKeyHash(groupKey)
@@ -2568,8 +2586,10 @@
 }
 
 @Suppress("UNCHECKED_CAST")
-/*inline */ class Updater<T>(val composer: Composer<*>, val node: T) {
-    @OptIn(ComposeCompilerApi::class)
+/*inline */ class Updater<T> @PublishedApi internal constructor(
+    @PublishedApi internal val composer: Composer<*>,
+    @PublishedApi internal val node: T
+) {
     inline fun set(
         value: Int,
         /*crossinline*/
@@ -2583,7 +2603,6 @@
         }
     }
 
-    @OptIn(ComposeCompilerApi::class)
     inline fun <reified V> set(
         value: V,
         /*crossinline*/
@@ -2597,7 +2616,6 @@
         }
     }
 
-    @OptIn(ComposeCompilerApi::class)
     inline fun update(
         value: Int,
         /*crossinline*/
@@ -2611,7 +2629,6 @@
         }
     }
 
-    @OptIn(ComposeCompilerApi::class)
     inline fun <reified V> update(
         value: V,
         /*crossinline*/
@@ -2632,8 +2649,10 @@
     }
 }
 
-class SkippableUpdater<T>(val composer: Composer<*>, val node: T) {
-    @OptIn(ComposeCompilerApi::class)
+class SkippableUpdater<T> @PublishedApi internal constructor(
+    @PublishedApi internal val composer: Composer<*>,
+    @PublishedApi internal val node: T
+) {
     inline fun update(block: Updater<T>.() -> Unit) {
         composer.startReplaceableGroup(0x1e65194f)
         Updater(composer, node).block()
@@ -2641,7 +2660,6 @@
     }
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotWriter.removeCurrentGroup(lifecycleManager: LifecycleManager) {
     // Notify the lifecycle manager of any observers leaving the slot table
     // The notification order should ensure that listeners are notified of leaving
@@ -2696,7 +2714,7 @@
 
 // Observation helpers
 
-// These helpers enable storing observaction pairs of value to scope instances in a list sorted by
+// These helpers enable storing observation pairs of value to scope instances in a list sorted by
 // the value hash as a primary key and the scope hash as the secondary key. This results in a
 // multi-set that allows finding an observation/scope pairs in O(log N) and a worst case of
 // insert into and remove from the array of O(log N) + O(N) where N is the number of total pairs.
@@ -2876,8 +2894,7 @@
 private fun Boolean.asInt() = if (this) 1 else 0
 private fun Int.asBool() = this != 0
 
-@Composable
-val currentComposer: Composer<*> get() {
+val currentComposer: Composer<*> @Composable get() {
     throw NotImplementedError("Implemented as an intrinsic")
 }
 
@@ -2896,7 +2913,6 @@
     return realFn(composer, 1)
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.distanceFrom(index: Int, root: Int): Int {
     var count = 0
     var current = index
@@ -2908,7 +2924,6 @@
 }
 
 // find the nearest common root
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.nearestCommonRootOf(a: Int, b: Int, common: Int): Int {
     // Early outs, to avoid calling distanceFrom in trivial cases
     if (a == b) return a // A group is the nearest common root of itself
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index 458e047..15b94e6 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -73,7 +73,7 @@
 ): Composition = Compositions.findOrCreate(key) {
     CompositionImpl(
         parent,
-        composerFactory = { slots, rcmpsr -> Composer(slots, applier, rcmpsr) },
+        composerFactory = { parent -> Composer(applier, parent) },
         onDispose = { Compositions.onDisposed(key) }
     ).also {
         onCreated()
@@ -87,11 +87,10 @@
  */
 private class CompositionImpl(
     private val parent: CompositionReference,
-    composerFactory: (SlotTable, CompositionReference) -> Composer<*>,
+    composerFactory: (CompositionReference) -> Composer<*>,
     private val onDispose: () -> Unit
 ) : Composition {
-    private val slotTable: SlotTable = SlotTable()
-    private val composer: Composer<*> = composerFactory(slotTable, parent).also {
+    private val composer: Composer<*> = composerFactory(parent).also {
         parent.registerComposer(it)
     }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionData.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionData.kt
new file mode 100644
index 0000000..53aa673
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionData.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime
+
+/**
+ * A [CompositionData] is the data tracked by the composer during composition.
+ *
+ * This interface is not intended to be used directly and is provided to allow the tools API to
+ * have access to data tracked during composition. The tools API should be used instead which
+ * provides a more usable interpretation of the slot table.
+ */
+interface CompositionData {
+    /**
+     * Iterate the composition data in the group.  The composition data is structured as a tree of
+     * values that corresponds to the call graph of the functions that produced the tree.
+     * Interspersed are groups that represents the nodes themselves.
+     */
+    val compositionGroups: Iterable<CompositionGroup>
+
+    /**
+     * Returns true if no composition data has been collected. This occurs when the first
+     * composition into this composition data has not completed yet or, if it is a group, it
+     * doesn't contain any child groups.
+     */
+    val isEmpty: Boolean
+}
+
+/**
+ * [CompositionGroup] is a group of data slots tracked independently by composition. These groups
+ * correspond to flow control branches (such as if statements and function calls) as well as
+ * emitting of a node to the tree.
+ *
+ * This interface is not intended to be used directly and is provided to allow the tools API to
+ * have access to data tracked during composition. The tools API should be used instead which
+ * provides a more usable interpretation of the slot table.
+ */
+interface CompositionGroup : CompositionData {
+    /**
+     * A value used to identify the group within its siblings and is typically a compiler
+     * generated integer but can be an object if the [key] composable is used.
+     */
+    val key: Any
+
+    /**
+     * Information recorded by the compiler to help tooling identify the source that generated
+     * the group. The format of this string is internal and is interpreted by the tools API which
+     * translates this information into source file name and offsets.
+     */
+    val sourceInfo: String?
+
+    /**
+     * If the group represents a node this returns a non-null value which is the node that was
+     * emitted for the group.
+     */
+    val node: Any?
+
+    /**
+     * The data stored in the slot table for this group. This information includes the values
+     * stored for parameters that are checked for change, any value passed as a parameter for
+     * [remember] and the last value returned by [remember], etc.
+     */
+    val data: Iterable<Any?>
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt
index 51f7320..cdcb110 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt
@@ -36,11 +36,12 @@
 abstract class CompositionReference internal constructor() {
     internal abstract val compoundHashKey: Int
     internal abstract val collectingKeySources: Boolean
+    internal abstract val collectingParameterInformation: Boolean
     internal abstract val effectCoroutineContext: CoroutineContext
     internal abstract fun composeInitial(composer: Composer<*>, composable: @Composable () -> Unit)
     internal abstract fun invalidate(composer: Composer<*>)
 
-    internal open fun recordInspectionTable(table: MutableSet<SlotTable>) {}
+    internal open fun recordInspectionTable(table: MutableSet<CompositionData>) {}
     internal open fun registerComposer(composer: Composer<*>) {
         registerComposerWithRoot(composer)
     }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
index ea257ff..5f2a2b37 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
@@ -197,8 +197,7 @@
  * An Effect to get the nearest invalidation lambda to the current point of composition. This can be used to
  * trigger an invalidation on the composition locally to cause a recompose.
  */
-@Composable
-val invalidate: () -> Unit get() {
+val invalidate: () -> Unit @Composable get() {
     val scope = currentComposer.currentRecomposeScope ?: error("no recompose scope found")
     scope.used = true
     return { scope.invalidate() }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
index d027d69..67ebb1b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
@@ -95,6 +95,32 @@
     currentComposer.endNode()
 }
 
+/**
+ * Emits a node into the composition of type [T]. Nodes emitted inside of [content] will become
+ * children of the emitted node.
+ *
+ * This function will throw a runtime exception if [E] is not a subtype of the applier of the
+ * [currentComposer].
+ *
+ * @sample androidx.compose.runtime.samples.CustomTreeComposition
+ *
+ * @param ctor A function which will create a new instance of [T]. This function is NOT
+ * guaranteed to be called in place.
+ * @param update A function to perform updates on the node. This will run every time emit is
+ * executed. This function is called in place and will be inlined.
+ * @param skippableUpdate A function to perform updates on the node. Unlike [update], this
+ * function is Composable and will therefore be skipped unless it has been invalidated by some
+ * other mechanism. This can be useful to perform expensive calculations for updating the node
+ * where the calculations are likely to have the same inputs over time, so the function's
+ * execution can be skipped.
+ * @param content the composable content that will emit the "children" of this node.
+ *
+ * @see Updater
+ * @see SkippableUpdater
+ * @see Applier
+ * @see emit
+ * @see compositionFor
+ */
 @Suppress("ComposableNaming")
 @OptIn(ComposeCompilerApi::class)
 @Composable @ComposableContract(readonly = true)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
index bfc4e99..31c52b0 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
@@ -32,19 +32,6 @@
 
 internal expect inline fun <R> synchronized(lock: Any, block: () -> R): R
 
-expect open class WeakReference<T> : Reference<T> {
-    constructor(referent: T)
-    constructor(referent: T, q: ReferenceQueue<in T>?)
-}
-
-expect abstract class Reference<T> {
-    open fun get(): T?
-}
-
-expect open class ReferenceQueue<T>() {
-    open fun poll(): Reference<out T>?
-}
-
 expect class AtomicReference<V>(value: V) {
     fun get(): V
     fun set(value: V)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt
deleted file mode 100644
index 1616cfc..0000000
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.runtime
-
-/**
- * A map from a key to a set of values used for keeping the relation between some
- * entities and a models changes of which this entities are observing.
- *
- * Two main differences from a regular Map<K, Set<V>>:
- * 1) Object.hashCode is not used, so the values can be mutable and change their hashCode value
- * 2) Objects are stored with WeakReference to prevent leaking them.
-*/
-class ObserverMap<K : Any, V : Any> {
-    private val keyToValue =
-        hashMapOf<IdentityWeakReference<K>, MutableSet<IdentityWeakReference<V>>>()
-    private val valueToKey =
-        hashMapOf<IdentityWeakReference<V>, MutableSet<IdentityWeakReference<K>>>()
-    private val keyQueue = ReferenceQueue<K>()
-    private val valueQueue = ReferenceQueue<V>()
-
-    /**
-     * Adds a [value] into a set associated with this [key].
-     */
-    fun add(key: K, value: V) {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key, keyQueue)
-        val weakValue = IdentityWeakReference(value, valueQueue)
-        addToSet(keyToValue, weakKey, weakValue)
-        addToSet(valueToKey, weakValue, weakKey)
-    }
-
-    /**
-     * Removes all the values associated with this [key].
-     *
-     * @return the list of values removed from the set as a result of this operation.
-     */
-    fun remove(key: K) {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key)
-        removeFromSet(keyToValue, valueToKey, weakKey)
-    }
-
-    /**
-     * Removes exact [value] from the set associated with this [key].
-     */
-    fun remove(key: K, value: V) {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key)
-        val weakValue = IdentityWeakReference(value)
-        keyToValue[weakKey]?.remove(weakValue)
-        valueToKey[weakValue]?.remove(weakKey)
-    }
-
-    /**
-     * Returns `true` when the map contains the given key and value
-     */
-    fun contains(key: K, value: V): Boolean {
-        clearReferences()
-        val set = keyToValue[IdentityWeakReference(key)]
-        return set?.contains(IdentityWeakReference(value)) ?: false
-    }
-
-    /**
-     * Clears all the keys and values from the map.
-     */
-    fun clear() {
-        keyToValue.clear()
-        valueToKey.clear()
-        clearReferences()
-    }
-
-    /**
-     * @return a list of values associated with the provided [keys].
-     */
-    operator fun get(keys: Iterable<K>): List<V> {
-        clearReferences()
-        val set = mutableSetOf<IdentityWeakReference<V>>()
-        keys.forEach { key ->
-            val weakKey = IdentityWeakReference(key)
-            keyToValue[weakKey]?.let(set::addAll)
-        }
-        return set.mapNotNull { it.get() }
-    }
-
-    /**
-     * @return a list of values associated with the provided [key]
-     */
-    fun getValueOf(key: K): List<V> {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key)
-        return keyToValue[weakKey]?.mapNotNull { it.get() }?.toList() ?: emptyList<V>()
-    }
-
-    /**
-     * Clears all the values that match the given [predicate] from all the sets.
-     */
-    @Suppress("UNCHECKED_CAST")
-    fun clearValues(predicate: (V) -> Boolean) {
-        clearReferences()
-        val matching = mutableListOf<V>()
-        valueToKey.keys.forEach { value ->
-            val v = value.get()
-            if (v != null && predicate(v)) {
-                matching.add(v)
-            }
-        }
-        matching.forEach { removeValue(it) }
-    }
-
-    /**
-     * Removes all values matching [value].
-     */
-    fun removeValue(value: V) {
-        clearReferences()
-        val weakValue = IdentityWeakReference(value)
-        valueToKey.remove(weakValue)?.forEach { key ->
-            val valueSet = keyToValue[key]!!
-            valueSet.remove(weakValue)
-            if (valueSet.isEmpty()) {
-                keyToValue.remove(key)
-            }
-        }
-    }
-
-    private fun clearReferences() {
-        pollQueue(keyQueue, keyToValue, valueToKey)
-        pollQueue(valueQueue, valueToKey, keyToValue)
-    }
-
-    private fun <T, U> pollQueue(
-        queue: ReferenceQueue<T>,
-        keyMap: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
-        valueMap: MutableMap<IdentityWeakReference<U>, MutableSet<IdentityWeakReference<T>>>
-    ) {
-        do {
-            val ref = queue.poll()
-            if (ref != null) {
-                @Suppress("UNCHECKED_CAST")
-                val weakKey = ref as IdentityWeakReference<T>
-                removeFromSet(keyMap, valueMap, weakKey)
-            }
-        } while (ref != null)
-    }
-
-    private fun <T, U> addToSet(
-        map: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
-        key: IdentityWeakReference<T>,
-        value: IdentityWeakReference<U>
-    ) {
-        var set = map[key]
-        if (set == null) {
-            set = hashSetOf()
-            map.put(key, set)
-        }
-        set.add(value)
-    }
-
-    private fun <T, U> removeFromSet(
-        mapFromKey: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
-        mapToKey: MutableMap<IdentityWeakReference<U>, MutableSet<IdentityWeakReference<T>>>,
-        key: IdentityWeakReference<T>
-    ) {
-        mapFromKey.remove(key)?.forEach { value ->
-            mapToKey[value]?.remove(key)
-        }
-    }
-}
-
-private class IdentityWeakReference<T>(value: T, queue: ReferenceQueue<T>? = null) :
-    WeakReference<T>(value, queue) {
-    val hash = identityHashCode(value)
-
-    override fun equals(other: Any?): Boolean {
-        if (other !is IdentityWeakReference<*>) {
-            return false
-        }
-        return hash == other.hash && get() === other.get()
-    }
-
-    override fun hashCode(): Int = hash
-}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index b543735..7f5560c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -433,7 +433,11 @@
     internal override val collectingKeySources: Boolean
         get() = false
 
-    internal override fun recordInspectionTable(table: MutableSet<SlotTable>) {
+    // Collecting parameter happens at the level of a composer; starts as false
+    internal override val collectingParameterInformation: Boolean
+        get() = false
+
+    internal override fun recordInspectionTable(table: MutableSet<CompositionData>) {
         // TODO: The root recomposer might be a better place to set up inspection
         // than the current configuration with an ambient
     }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index f5e27ed..14e4ca6 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -73,8 +73,7 @@
 
 // The public API refers only to Index values. Address values are internal.
 
-@InternalComposeApi
-class SlotTable {
+internal class SlotTable : CompositionData, Iterable<CompositionGroup> {
     /**
      * An array to store group information that is stored as groups of [Group_Fields_Size]
      * elements of the array. The [groups] array can be thought of as an array of an inline
@@ -113,7 +112,14 @@
     /**
      * Tracks whether there is an active writer.
      */
-    private var writer = false
+    internal var writer = false
+        private set
+
+    /**
+     * An internal version that is incremented whenever a writer is created. This is used to
+     * detect when an iterator created by [CompositionData] is invalid.
+     */
+    internal var version = 0
 
     /**
      * A list of currently active anchors.
@@ -123,7 +129,7 @@
     /**
      * Returns true if the slot table is empty
      */
-    val isEmpty get() = groupsSize == 0
+    override val isEmpty get() = groupsSize == 0
 
     /**
      * Read the slot table in [block]. Any number of readers can be created but a slot table cannot
@@ -178,6 +184,7 @@
         check(!writer) { "Cannot start a writer when another writer is pending" }
         check(readers <= 0) { "Cannot start a writer when a reader is pending" }
         writer = true
+        version++
         return SlotWriter(this)
     }
 
@@ -438,6 +445,11 @@
         val end = if (group + 1 < groupsSize) groups.dataAnchor(group + 1) else slots.size
         return slots.toList().subList(start, end)
     }
+
+    override val compositionGroups: Iterable<CompositionGroup> get() = this
+
+    override fun iterator(): Iterator<CompositionGroup> =
+        GroupIterator(this, 0, groupsSize)
 }
 
 /**
@@ -448,8 +460,7 @@
  * instead of the [SlotTable] as the anchor index could have shifted due to operations performed
  * on the writer.
  */
-@InternalComposeApi
-class Anchor internal constructor(loc: Int) {
+internal class Anchor(loc: Int) {
     internal var location: Int = loc
     val valid get() = location != Int.MIN_VALUE
     fun toIndexFor(slots: SlotTable) = slots.anchorIndex(this)
@@ -459,8 +470,7 @@
 /**
  * A reader of a slot table. See [SlotTable]
  */
-@InternalComposeApi
-class SlotReader(
+internal class SlotReader(
     /**
      * The table for whom this is a reader.
      */
@@ -773,7 +783,7 @@
     fun reposition(index: Int) {
         require(emptyCount == 0) { "Cannot reposition while in an empty region" }
         currentGroup = index
-        val parent = groups.parentAnchor(index)
+        val parent = if (index < groupsSize) groups.parentAnchor(index) else -1
         this.parent = parent
         if (parent < 0)
             this.currentEnd = groupsSize
@@ -890,8 +900,7 @@
 /**
  * The writer for a slot table. See [SlotTable] for details.
  */
-@InternalComposeApi
-class SlotWriter internal constructor(
+internal class SlotWriter(
     /**
      * The [SlotTable] for whom this is writer.
      */
@@ -2350,6 +2359,79 @@
         if (index > parentAnchorPivot) index else size + index - parentAnchorPivot
 }
 
+private class GroupIterator(
+    val table: SlotTable,
+    start: Int,
+    val end: Int
+) : Iterator<CompositionGroup> {
+    private var index = start
+    private val version = table.version
+
+    init {
+        if (table.writer) throw ConcurrentModificationException()
+    }
+
+    override fun hasNext() = index < end
+
+    override fun next(): CompositionGroup {
+        validateRead()
+        val group = index
+        index += table.groups.groupSize(group)
+        return object : CompositionGroup, Iterable<CompositionGroup> {
+            override val isEmpty: Boolean get() = table.groups.groupSize(group) == 0
+
+            override val key: Any
+                get() = if (table.groups.hasObjectKey(group))
+                    table.slots[table.groups.objectKeyIndex(group)]!!
+                else table.groups.key(group)
+
+            override val sourceInfo: String?
+                get() = if (table.groups.hasAux(group))
+                    table.slots[table.groups.auxIndex(group)] as? String
+                else null
+
+            override val node: Any?
+                get() = if (table.groups.isNode(group))
+                    table.slots[table.groups.nodeIndex(group)] else
+                    null
+
+            override val data: Iterable<Any?> get() {
+                val start = table.groups.dataAnchor(group)
+                val end = if (group + 1 < table.groupsSize)
+                    table.groups.dataAnchor(group + 1) else table.slotsSize
+                return object : Iterable<Any?>, Iterator<Any?> {
+                    var index = start
+                    override fun iterator(): Iterator<Any?> = this
+                    override fun hasNext(): Boolean = index < end
+                    override fun next(): Any? =
+                        (
+                            if (index >= 0 && index < table.slots.size)
+                                table.slots[index]
+                            else null
+                            ).also { index++ }
+                }
+            }
+
+            override val compositionGroups: Iterable<CompositionGroup> get() = this
+
+            override fun iterator(): Iterator<CompositionGroup> {
+                validateRead()
+                return GroupIterator(
+                    table,
+                    group + 1,
+                    group + table.groups.groupSize(group)
+                )
+            }
+        }
+    }
+
+    private fun validateRead() {
+        if (table.version != version) {
+            throw ConcurrentModificationException()
+        }
+    }
+}
+
 // Parent -1 is reserved to be the root parent index so the anchor must pivot on -2.
 private const val parentAnchorPivot = -2
 
@@ -2539,7 +2621,6 @@
 /**
  * This is inlined here instead to avoid allocating a lambda for the compare when this is used.
  */
-@OptIn(InternalComposeApi::class)
 private fun ArrayList<Anchor>.search(location: Int, effectiveSize: Int): Int {
     var low = 0
     var high = size - 1
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
index 044bbab..e760c75 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
@@ -1177,7 +1177,7 @@
 private fun RecomposeScope?.replacableWith(other: RecomposeScope) =
     this == null || !this.valid || this == other || this.anchor == other.anchor
 
-@ComposeCompilerApi
+@OptIn(ComposeCompilerApi::class)
 private typealias CLambda = ComposableLambda<Any, Any, Any, Any, Any, Any, Any, Any, Any, Any,
     Any, Any, Any, Any, Any, Any, Any, Any, Any>
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt
index eecd738..b00fb76 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt
@@ -51,7 +51,13 @@
 private val liveLiteralCache = HashMap<String, MutableState<Any?>>()
 
 @InternalComposeApi
-val isLiveLiteralsEnabled: Boolean = false
+var isLiveLiteralsEnabled: Boolean = false
+    private set
+
+@InternalComposeApi
+fun enableLiveLiterals() {
+    isLiveLiteralsEnabled = true
+}
 
 @InternalComposeApi
 fun <T> liveLiteral(key: String, value: T): State<T> {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
index aa29075..132b707 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
@@ -88,9 +88,11 @@
     override fun clear() = writable { list = persistentListOf() }
     override fun remove(element: T) = conditionalUpdate { it.remove(element) }
     override fun removeAll(elements: Collection<T>) = conditionalUpdate { it.removeAll(elements) }
-    override fun removeAt(index: Int) = get(index).also { update { it.removeAt(index) } }
+    override fun removeAt(index: Int): T = get(index).also { update { it.removeAt(index) } }
     override fun retainAll(elements: Collection<T>) = mutate { it.retainAll(elements) }
-    override fun set(index: Int, element: T) = get(index).also { update { it.set(index, element) } }
+    override fun set(index: Int, element: T): T = get(index).also {
+        update { it.set(index, element) }
+    }
 
     fun removeRange(fromIndex: Int, toIndex: Int) {
         mutate {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/InspectionTables.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/InspectionTables.kt
index 6a1b9db..e8e35cc 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/InspectionTables.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/InspectionTables.kt
@@ -16,12 +16,12 @@
 
 package androidx.compose.runtime.tooling
 
+import androidx.compose.runtime.CompositionData
 import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.SlotTable
 import androidx.compose.runtime.staticAmbientOf
 
 /**
  * A set of slot tables that where produced when in inspection mode.
  */
 @InternalComposeApi
-val InspectionTables = staticAmbientOf<MutableSet<SlotTable>?> { null }
+val InspectionTables = staticAmbientOf<MutableSet<CompositionData>?> { null }
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
index 927fa46..830f020 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
@@ -45,10 +45,4 @@
     }
 }
 
-internal actual typealias Reference<T> = java.lang.ref.Reference<T>
-
-internal actual typealias ReferenceQueue<T> = java.lang.ref.ReferenceQueue<T>
-
-internal actual typealias WeakReference<T> = java.lang.ref.WeakReference<T>
-
 internal actual typealias TestOnly = org.jetbrains.annotations.TestOnly
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt
index e80dbdb..59fcfd2 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt
@@ -35,22 +35,22 @@
 
     @Test fun downGoesDown() {
         val one = Node("one")
-        applier.insert(0, one)
+        applier.insertTopDown(0, one)
         applier.down(one)
         assertSame(one, applier.current)
 
         val two = Node("two")
-        applier.insert(0, two)
+        applier.insertTopDown(0, two)
         applier.down(two)
         assertSame(two, applier.current)
     }
 
     @Test fun upGoesUp() {
         val one = Node("one")
-        applier.insert(0, one)
+        applier.insertTopDown(0, one)
         applier.down(one)
         val two = Node("two")
-        applier.insert(0, two)
+        applier.insertTopDown(0, two)
         applier.down(two)
 
         applier.up()
@@ -61,7 +61,7 @@
 
     @Test fun clearClearsAndPointsToRoot() {
         val child = Node("child")
-        applier.insert(0, child)
+        applier.insertTopDown(0, child)
         applier.down(child)
 
         applier.clear()
@@ -76,10 +76,10 @@
         val two = Node("two")
         val three = Node("three")
         val four = Node("four")
-        applier.insert(0, one)
-        applier.insert(1, two)
-        applier.insert(2, three)
-        applier.insert(3, four)
+        applier.insertTopDown(0, one)
+        applier.insertTopDown(1, two)
+        applier.insertTopDown(2, three)
+        applier.insertTopDown(3, four)
 
         applier.remove(1, 1) // Middle
         assertEquals(listOf(one, three, four), root.children)
@@ -99,13 +99,13 @@
         val five = Node("five")
         val six = Node("six")
         val seven = Node("seven")
-        applier.insert(0, one)
-        applier.insert(1, two)
-        applier.insert(2, three)
-        applier.insert(3, four)
-        applier.insert(4, five)
-        applier.insert(5, six)
-        applier.insert(6, seven)
+        applier.insertTopDown(0, one)
+        applier.insertTopDown(1, two)
+        applier.insertTopDown(2, three)
+        applier.insertTopDown(3, four)
+        applier.insertTopDown(4, five)
+        applier.insertTopDown(5, six)
+        applier.insertTopDown(6, seven)
 
         applier.remove(2, 2) // Middle
         assertEquals(listOf(one, two, five, six, seven), root.children)
@@ -121,9 +121,9 @@
         val one = Node("one")
         val two = Node("two")
         val three = Node("three")
-        applier.insert(0, one)
-        applier.insert(1, two)
-        applier.insert(2, three)
+        applier.insertTopDown(0, one)
+        applier.insertTopDown(1, two)
+        applier.insertTopDown(2, three)
 
         applier.move(0, 3, 1)
         assertEquals(listOf(two, three, one), root.children)
@@ -141,9 +141,9 @@
         val one = Node("one")
         val two = Node("two")
         val three = Node("three")
-        applier.insert(0, one)
-        applier.insert(1, two)
-        applier.insert(2, three)
+        applier.insertTopDown(0, one)
+        applier.insertTopDown(1, two)
+        applier.insertTopDown(2, three)
 
         applier.move(2, 0, 1)
         assertEquals(listOf(three, one, two), root.children)
@@ -162,10 +162,10 @@
         val two = Node("two")
         val three = Node("three")
         val four = Node("four")
-        applier.insert(0, one)
-        applier.insert(1, two)
-        applier.insert(2, three)
-        applier.insert(3, four)
+        applier.insertTopDown(0, one)
+        applier.insertTopDown(1, two)
+        applier.insertTopDown(2, three)
+        applier.insertTopDown(3, four)
 
         applier.move(0, 4, 2)
         assertEquals(listOf(three, four, one, two), root.children)
@@ -178,10 +178,10 @@
         val two = Node("two")
         val three = Node("three")
         val four = Node("four")
-        applier.insert(0, one)
-        applier.insert(1, two)
-        applier.insert(2, three)
-        applier.insert(3, four)
+        applier.insertTopDown(0, one)
+        applier.insertTopDown(1, two)
+        applier.insertTopDown(2, three)
+        applier.insertTopDown(3, four)
 
         applier.move(2, 0, 2)
         assertEquals(listOf(three, four, one, two), root.children)
@@ -195,10 +195,12 @@
 
 @OptIn(ExperimentalComposeApi::class)
 private class NodeApplier(root: Node) : AbstractApplier<Node>(root) {
-    override fun insert(index: Int, instance: Node) {
+    override fun insertTopDown(index: Int, instance: Node) {
         current.children.add(index, instance)
     }
 
+    override fun insertBottomUp(index: Int, instance: Node) { }
+
     override fun remove(index: Int, count: Int) {
         current.children.remove(index, count)
     }
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionDataTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionDataTests.kt
new file mode 100644
index 0000000..20ee054
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionDataTests.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@OptIn(InternalComposeApi::class)
+class CompositionDataTests {
+
+    @Test
+    fun canGetCompositionDataFromSlotTable() {
+        val slots = SlotTable()
+        val compositionData = slots as CompositionData
+        assertTrue(compositionData.compositionGroups.toList().isEmpty())
+    }
+
+    @Test
+    fun canIterateASlotTable() {
+        val slots = SlotTable().also {
+            it.write { writer ->
+                writer.insert {
+                    writer.group(1) {
+                        for (i in 1..5) {
+                            writer.group(i * 10) {
+                                for (j in 1..i) {
+                                    writer.update(i * 100 + j)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        slots.verifyWellFormed()
+
+        val list = mutableListOf<Int>()
+        fun iterate(compositionData: CompositionData) {
+            for (group in compositionData.compositionGroups) {
+                list.add(group.key as Int)
+                for (data in group.data) {
+                    list.add(data as Int)
+                }
+                iterate(group)
+            }
+        }
+
+        iterate(slots)
+
+        assertEquals(
+            listOf(
+                1, 10, 101,
+                20, 201, 202,
+                30, 301, 302, 303,
+                40, 401, 402, 403, 404,
+                50, 501, 502, 503, 504, 505
+            ),
+            list
+        )
+    }
+
+    @Test
+    fun canFindNodes() {
+        val data = List(26) { 'A' + it }
+        val slots = SlotTable().also {
+            it.write { writer ->
+                writer.insert {
+                    writer.group(0) {
+                        fun emit(a: List<Char>) {
+                            if (a.isNotEmpty()) {
+                                writer.group(1) {
+                                    val mid = (a.size - 1) / 2 + 1
+                                    writer.nodeGroup(10, a[0])
+                                    if (mid > 1)
+                                        emit(a.subList(1, mid))
+                                    if (mid < a.size)
+                                        emit(a.subList(mid, a.size))
+                                }
+                            }
+                        }
+
+                        emit(data)
+                    }
+                }
+            }
+        }
+
+        val collected = mutableListOf<Char>()
+
+        fun collect(data: CompositionData) {
+            for (group in data.compositionGroups) {
+                if (group.node != null) {
+                    collected.add(group.node as Char)
+                }
+                collect(group)
+            }
+        }
+
+        collect(slots)
+
+        assertEquals(data, collected)
+    }
+
+    @Test
+    fun canFindSourceInfo() {
+        val slots = SlotTable().also {
+            var data = 0
+            it.write { writer ->
+                writer.insert {
+                    writer.group(0) {
+                        fun emit(depth: Int) {
+                            if (depth == 0) {
+                                writer.startData(100, aux = "$data")
+                                data++
+                                writer.endGroup()
+                            } else {
+                                if (depth == 2) {
+                                    writer.startData(depth * 1000, aux = "$data")
+                                    data++
+                                } else writer.startGroup(depth)
+                                emit(depth - 1)
+                                emit(depth - 1)
+                                writer.endGroup()
+                            }
+                        }
+                        emit(5)
+                    }
+                }
+            }
+        }
+
+        val collected = mutableListOf<String>()
+
+        fun collect(data: CompositionData) {
+            for (group in data.compositionGroups) {
+                val sourceInfo = group.sourceInfo
+                if (sourceInfo != null) {
+                    collected.add(sourceInfo)
+                }
+                collect(group)
+            }
+        }
+
+        collect(slots)
+
+        assertEquals(List(40) { "$it" }, collected)
+    }
+
+    @Test(expected = ConcurrentModificationException::class)
+    fun writeDuringIterationCausesException() {
+        val slots = SlotTable().also {
+            it.write { writer ->
+                writer.insert {
+                    writer.group(0) {
+                        repeat(10) { index ->
+                            writer.group(100 + index) { }
+                        }
+                    }
+                }
+            }
+        }
+
+        fun insertAGroup() {
+            slots.write { writer ->
+                writer.group {
+                    repeat(3) { writer.group { } }
+                    writer.insert {
+                        writer.group(200) { }
+                    }
+                    writer.skipToGroupEnd()
+                }
+            }
+        }
+
+        val groups = slots.compositionGroups.iterator()
+        insertAGroup()
+
+        // Expect this to cause an exception
+        groups.next()
+    }
+
+    @Test(expected = ConcurrentModificationException::class)
+    fun iterationDuringWriteCausesException() {
+        val slots = SlotTable().also {
+            it.write { writer ->
+                writer.insert {
+                    writer.group(0) {
+                        repeat(10) { index ->
+                            writer.group(100 + index) { }
+                        }
+                    }
+                }
+            }
+        }
+
+        slots.write { writer ->
+            writer.group {
+                repeat(3) { writer.group { } }
+                writer.insert {
+                    writer.group(200) { }
+                }
+                writer.skipToGroupEnd()
+
+                // Expect this to throw an exception
+                slots.compositionGroups.first()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 1374742b..84a2b9f 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.mock.MockViewValidator
 import androidx.compose.runtime.mock.Point
 import androidx.compose.runtime.mock.Report
+import androidx.compose.runtime.mock.TestMonotonicFrameClock
 import androidx.compose.runtime.mock.View
 import androidx.compose.runtime.mock.ViewApplier
 import androidx.compose.runtime.mock.contact
@@ -41,9 +42,12 @@
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.takeMutableSnapshot
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.runBlockingTest
 import kotlin.test.AfterTest
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -317,7 +321,7 @@
             repeat(of = chars) { c -> textOf(c) }
         }
 
-        fun MockViewValidator.validatechars(chars: Iterable<Char>) {
+        fun MockViewValidator.validateChars(chars: Iterable<Char>) {
             repeat(of = chars) { c -> textOf(c) }
         }
 
@@ -329,9 +333,9 @@
         }
 
         result.validate {
-            validatechars(chars)
-            validatechars(chars)
-            validatechars(chars)
+            validateChars(chars)
+            validateChars(chars)
+            validateChars(chars)
         }
 
         chars = listOf('a', 'b', 'x', 'c')
@@ -339,9 +343,9 @@
         result.expectChanges()
 
         result.validate {
-            validatechars(chars)
-            validatechars(chars)
-            validatechars(chars)
+            validateChars(chars)
+            validateChars(chars)
+            validateChars(chars)
         }
     }
 
@@ -563,7 +567,7 @@
     }
 
     @Test
-    fun testComponentWithVarCtorParameter() {
+    fun testComponentWithVarConstructorParameter() {
         @Composable fun MockComposeScope.One(first: Int) {
             text("$first")
         }
@@ -597,7 +601,7 @@
     }
 
     @Test
-    fun testComponentWithValCtorParameter() {
+    fun testComponentWithValConstructorParameter() {
         @Composable fun MockComposeScope.One(first: Int) {
             text("$first")
         }
@@ -2503,6 +2507,129 @@
             validate()
         }
     }
+
+    /**
+     * This test checks that an updated ComposableLambda capture used in a subcomposition
+     * correctly invalidates that subcomposition and schedules recomposition of that subcomposition.
+     */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testComposableLambdaSubcompositionInvalidation() = runBlockingTest {
+        localRecomposerTest { recomposer ->
+            val composer = Composer(EmptyApplier(), recomposer)
+            try {
+                var rootState by mutableStateOf(false)
+                val composedResults = mutableListOf<Boolean>()
+                Snapshot.notifyObjectsInitialized()
+                recomposer.composeInitial(composer) {
+                    // Read into local variable, local will be captured below
+                    val capturedValue = rootState
+                    TestSubcomposition {
+                        composedResults.add(capturedValue)
+                    }
+                }
+                composer.applyChanges()
+                assertEquals(listOf(false), composedResults)
+                rootState = true
+                Snapshot.sendApplyNotifications()
+                advanceUntilIdle()
+                assertEquals(listOf(false, true), composedResults)
+            } finally {
+                composer.dispose()
+            }
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testCompositionReferenceIsRemembered() = runBlockingTest {
+        localRecomposerTest { recomposer ->
+            val composer = Composer(EmptyApplier(), recomposer)
+            try {
+                lateinit var invalidator: () -> Unit
+                val parentReferences = mutableListOf<CompositionReference>()
+                recomposer.composeInitial(composer) {
+                    invalidator = invalidate
+                    parentReferences += compositionReference()
+                }
+                composer.applyChanges()
+                invalidator()
+                advanceUntilIdle()
+                assert(parentReferences.size > 1) { "expected to be composed more than once" }
+                assert(parentReferences.toSet().size == 1) {
+                    "expected all parentReferences to be the same; saw $parentReferences"
+                }
+            } finally {
+                composer.dispose()
+            }
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun testParentCompositionRecomposesFirst() = runBlockingTest {
+        localRecomposerTest { recomposer ->
+            val composer = Composer(EmptyApplier(), recomposer)
+            val results = mutableListOf<String>()
+            try {
+                var firstState by mutableStateOf("firstInitial")
+                var secondState by mutableStateOf("secondInitial")
+                Snapshot.notifyObjectsInitialized()
+                recomposer.composeInitial(composer) {
+                    results += firstState
+                    TestSubcomposition {
+                        results += secondState
+                    }
+                }
+                secondState = "secondSet"
+                Snapshot.sendApplyNotifications()
+                firstState = "firstSet"
+                Snapshot.sendApplyNotifications()
+                advanceUntilIdle()
+                assertEquals(
+                    listOf("firstInitial", "secondInitial", "firstSet", "secondSet"),
+                    results,
+                    "Expected call ordering during recomposition of subcompositions"
+                )
+            } finally {
+                composer.dispose()
+            }
+        }
+    }
+}
+
+@OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
+@Composable
+private fun TestSubcomposition(
+    content: @Composable () -> Unit
+) {
+    val parentRef = compositionReference()
+    val currentContent by rememberUpdatedState(content)
+    DisposableEffect(parentRef) {
+        val subcomposer = Composer(EmptyApplier(), parentRef)
+        parentRef.composeInitial(subcomposer) {
+            currentContent()
+        }
+        subcomposer.applyChanges()
+        onDispose {
+            subcomposer.dispose()
+        }
+    }
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+private suspend fun <R> localRecomposerTest(
+    block: CoroutineScope.(Recomposer) -> R
+) = coroutineScope {
+    val contextWithClock = coroutineContext + TestMonotonicFrameClock(this)
+    val recomposer = Recomposer(contextWithClock)
+    launch(contextWithClock) {
+        recomposer.runRecomposeAndApplyChanges()
+    }
+    block(recomposer)
+    // This call doesn't need to be in a finally; everything it does will be torn down
+    // in exceptional cases by the coroutineScope failure
+    recomposer.shutDown()
 }
 
 @Composable fun Wrap(content: @Composable () -> Unit) {
@@ -2544,8 +2671,7 @@
         assertTrue(changes, "Expected changes")
         composer.applyChanges()
         Snapshot.notifyObjectsInitialized()
-        composer.slotTable.verifyWellFormed()
-        composer.insertTable.verifyWellFormed()
+        composer.verifyConsistent()
     }
 
     fun recompose(): Boolean = doCompose(composer) { composer.recompose() }
@@ -2581,6 +2707,9 @@
     }
 }
 
+/**
+ * Composer the given block.
+ */
 @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
 private fun compose(
     block: @Composable MockComposeScope.() -> Unit
@@ -2600,7 +2729,6 @@
             scope.launch(clock) { runRecomposeAndApplyChanges() }
         }
         Composer(
-            SlotTable(),
             ViewApplier(root),
             recomposer
         )
@@ -2614,26 +2742,11 @@
         Snapshot.notifyObjectsInitialized()
         composer.applyChanges()
     }
-    composer.slotTable.verifyWellFormed()
-    validateRecomposeScopeAnchors(composer.slotTable)
+    composer.verifyConsistent()
 
     return CompositionResult(composer, root)
 }
 
-@OptIn(InternalComposeApi::class)
-fun validateRecomposeScopeAnchors(slotTable: SlotTable) {
-    val scopes = slotTable.slots.map { it as? RecomposeScope }.filterNotNull()
-    for (scope in scopes) {
-        scope.anchor?.let { anchor ->
-            check(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
-                val dataIndex = slotTable.slots.indexOf(scope)
-                "Misaligned anchor $anchor in scope $scope encountered, scope found at " +
-                    "$dataIndex"
-            }
-        }
-    }
-}
-
 // Contact test data
 private val bob = Contact("Bob Smith", email = "bob@smith.com")
 private val jon = Contact(name = "Jon Alberton", email = "jon@alberton.com")
@@ -2664,3 +2777,23 @@
 private interface Named {
     val name: String
 }
+
+@OptIn(ExperimentalComposeApi::class)
+private class EmptyApplier : Applier<Unit> {
+    override val current: Unit = Unit
+    override fun down(node: Unit) {}
+    override fun up() {}
+    override fun insertTopDown(index: Int, instance: Unit) {
+        error("Unexpected")
+    }
+    override fun insertBottomUp(index: Int, instance: Unit) {
+        error("Unexpected")
+    }
+    override fun remove(index: Int, count: Int) {
+        error("Unexpected")
+    }
+    override fun move(from: Int, to: Int, count: Int) {
+        error("Unexpected")
+    }
+    override fun clear() {}
+}
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt
deleted file mode 100644
index 8828ff6..0000000
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.runtime
-
-import kotlin.test.BeforeTest
-import kotlin.test.Test
-import kotlin.test.assertEquals
-
-class ObserverMapTests {
-
-    private val node1 = 1
-    private val node2 = 2
-    private lateinit var map: ObserverMap<TestModel, Int>
-
-    @BeforeTest
-    fun setup() {
-        map = ObserverMap()
-    }
-
-    @Test
-    fun testMapContainsPreviouslyAddedModel() {
-        val model = TestModel()
-        map.add(model, node1)
-
-        map.assertNodes(model, node1)
-    }
-
-    @Test
-    fun testMapAssociateBothNodesWithTheModel() {
-        val model = TestModel()
-        map.add(model, node1)
-        map.add(model, node2)
-
-        map.assertNodes(model, node1, node2)
-    }
-
-    @Test
-    fun testMapContainsModelWithChangedHashCode() {
-        val model = TestModel("Original")
-        map.add(model, node1)
-        model.content = "Changed"
-
-        map.assertNodes(model, node1)
-    }
-
-    @Test
-    fun testMapRemovesTheModel() {
-        val model = TestModel()
-        map.add(model, node1)
-        map.add(model, node2)
-
-        map.remove(model)
-
-        map.assertNodes(model)
-    }
-
-    @Test
-    fun testMapRemovesTheNode() {
-        val model = TestModel()
-        map.add(model, node1)
-        map.add(model, node2)
-
-        map.remove(model, node1)
-
-        map.assertNodes(model, node2)
-    }
-
-    @Test
-    fun testMapClearsAllTheModels() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        map.add(model1, node1)
-        map.add(model2, node2)
-
-        map.clear()
-
-        map.assertNodes(model1)
-        map.assertNodes(model2)
-    }
-
-    @Test
-    fun testMapClearsTheValuesByPredicate() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        val node3 = 3
-        map.add(model1, node1)
-        map.add(model2, node2)
-        map.add(model2, node3)
-
-        map.clearValues { it == node1 || it == node3 }
-
-        map.assertNodes(model1)
-        map.assertNodes(model2, node2)
-    }
-
-    @Test
-    fun testGetForMultipleModels() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        val model3 = TestModel("Test2")
-        val node3 = 3
-        val node4 = 4
-        map.add(model1, node1)
-        map.add(model1, node2)
-        map.add(model2, node3)
-        map.add(model3, node4)
-
-        map.assertNodes(listOf(model1, model2, model3), node1, node2, node3, node4)
-    }
-
-    @Test
-    fun testGetFiltersDuplicates() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        map.add(model1, node1)
-        map.add(model2, node1)
-
-        map.assertNodes(listOf(model1, model2), node1)
-    }
-
-    @Test
-    fun testRemoveValue() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        map.add(model1, node1)
-        map.add(model2, node1)
-        map.add(model2, node2)
-
-        map.removeValue(node1)
-        map.assertNodes(listOf(model2), node2)
-    }
-
-    private data class TestModel(var content: String = "Test")
-
-    private fun ObserverMap<TestModel, Int>.assertNodes(
-        model: TestModel,
-        vararg nodes: Int
-    ) {
-        assertNodes(listOf(model), *nodes)
-    }
-
-    private fun ObserverMap<TestModel, Int>.assertNodes(
-        models: List<TestModel>,
-        vararg nodes: Int
-    ) {
-        val expected = nodes.toList().sorted()
-        val actual = get(models).sorted()
-        assertEquals(expected, actual)
-    }
-}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt
index 2f61e12..d6d2f38 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt
@@ -3047,37 +3047,57 @@
             }
         }
     }
+
+    @Test
+    fun canRepositionReaderPastEndOfTable() {
+        val slots = SlotTable().also {
+            it.write { writer ->
+                // Create exactly 256 groups
+                repeat(256) {
+                    writer.insert {
+                        writer.startGroup(0)
+                        writer.endGroup()
+                    }
+                }
+            }
+        }
+
+        slots.read { reader ->
+            reader.reposition(reader.size)
+            // Expect the above not to crash.
+        }
+    }
 }
 
 @OptIn(InternalComposeApi::class)
-private inline fun SlotWriter.group(block: () -> Unit) {
+internal inline fun SlotWriter.group(block: () -> Unit) {
     startGroup()
     block()
     endGroup()
 }
 
 @OptIn(InternalComposeApi::class)
-private inline fun SlotWriter.group(key: Int, block: () -> Unit) {
+internal inline fun SlotWriter.group(key: Int, block: () -> Unit) {
     startGroup(key)
     block()
     endGroup()
 }
 
 @OptIn(InternalComposeApi::class)
-private inline fun SlotWriter.nodeGroup(key: Int, node: Any, block: () -> Unit = { }) {
+internal inline fun SlotWriter.nodeGroup(key: Int, node: Any, block: () -> Unit = { }) {
     startNode(key, node)
     block()
     endGroup()
 }
 @OptIn(InternalComposeApi::class)
-private inline fun SlotWriter.insert(block: () -> Unit) {
+internal inline fun SlotWriter.insert(block: () -> Unit) {
     beginInsert()
     block()
     endInsert()
 }
 
 @OptIn(InternalComposeApi::class)
-private inline fun SlotReader.group(key: Int, block: () -> Unit) {
+internal inline fun SlotReader.group(key: Int, block: () -> Unit) {
     assertEquals(key, groupKey)
     startGroup()
     block()
@@ -3085,7 +3105,7 @@
 }
 
 @OptIn(InternalComposeApi::class)
-private inline fun SlotReader.group(block: () -> Unit) {
+internal inline fun SlotReader.group(block: () -> Unit) {
     startGroup()
     block()
     endGroup()
@@ -3104,7 +3124,7 @@
 private const val elementKey = 100
 
 @OptIn(InternalComposeApi::class)
-fun testSlotsNumbered(): SlotTable {
+private fun testSlotsNumbered(): SlotTable {
     val slotTable = SlotTable()
     slotTable.write { writer ->
         writer.beginInsert()
@@ -3121,7 +3141,7 @@
 
 // Creates 0 until 10 items each with 10 elements numbered 0...n with 0..n slots
 @OptIn(InternalComposeApi::class)
-fun testItems(): SlotTable {
+private fun testItems(): SlotTable {
     val slots = SlotTable()
     slots.write { writer ->
         writer.beginInsert()
@@ -3158,7 +3178,7 @@
 }
 
 @OptIn(InternalComposeApi::class)
-fun validateItems(slots: SlotTable) {
+private fun validateItems(slots: SlotTable) {
     slots.read { reader ->
         check(reader.groupKey == treeRoot) { "Invalid root key" }
         reader.startGroup()
@@ -3209,7 +3229,7 @@
 }
 
 @OptIn(InternalComposeApi::class)
-fun narrowTrees(): Pair<SlotTable, List<Anchor>> {
+private fun narrowTrees(): Pair<SlotTable, List<Anchor>> {
     val slots = SlotTable()
     val anchors = mutableListOf<Anchor>()
     slots.write { writer ->
@@ -3258,13 +3278,13 @@
 }
 
 @OptIn(InternalComposeApi::class)
-fun SlotReader.expectGroup(key: Int): Int {
+private fun SlotReader.expectGroup(key: Int): Int {
     assertEquals(key, groupKey)
     return skipGroup()
 }
 
 @OptIn(InternalComposeApi::class)
-fun SlotReader.expectGroup(
+private fun SlotReader.expectGroup(
     key: Int,
     block: () -> Unit
 ) {
@@ -3275,12 +3295,12 @@
 }
 
 @OptIn(InternalComposeApi::class)
-fun SlotReader.expectData(value: Any) {
+private fun SlotReader.expectData(value: Any) {
     assertEquals(value, next())
 }
 
 @OptIn(InternalComposeApi::class)
-fun SlotReader.expectGroup(
+private fun SlotReader.expectGroup(
     key: Int,
     objectKey: Any?,
     block: () -> Unit = { skipToGroupEnd() }
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/TestMonotonicFrameClock.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/TestMonotonicFrameClock.kt
new file mode 100644
index 0000000..d7d6eb2
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/TestMonotonicFrameClock.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * NOTE: This file is copied from androidx.compose.ui.test for use in testing compose-runtime.
+ * A future patch may graduate this to a formal compose-runtime-test module.
+ */
+package androidx.compose.runtime.mock
+
+import androidx.compose.runtime.dispatch.MonotonicFrameClock
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.DelayController
+import kotlin.coroutines.ContinuationInterceptor
+
+private const val DefaultFrameDelay = 16_000_000L
+
+/**
+ * Construct a [TestMonotonicFrameClock] for [coroutineScope], obtaining the [DelayController]
+ * from the scope's [context][CoroutineScope.coroutineContext]. This frame clock may be used to
+ * consistently drive time under controlled tests.
+ *
+ * Calls to [TestMonotonicFrameClock.withFrameNanos] will schedule an upcoming frame
+ * [frameDelayNanos] nanoseconds in the future by launching into [coroutineScope] if such a frame
+ * has not yet been scheduled.
+ */
+@Suppress("MethodNameUnits") // Nanos for high-precision animation clocks
+@ExperimentalCoroutinesApi
+fun TestMonotonicFrameClock(
+    coroutineScope: CoroutineScope,
+    frameDelayNanos: Long = DefaultFrameDelay
+): TestMonotonicFrameClock = TestMonotonicFrameClock(
+    coroutineScope = coroutineScope,
+    delayController = coroutineScope.coroutineContext[ContinuationInterceptor].let { interceptor ->
+        requireNotNull(interceptor as? DelayController) {
+            "ContinuationInterceptor $interceptor of supplied scope must implement DelayController"
+        }
+    },
+    frameDelayNanos = frameDelayNanos
+)
+
+/**
+ * A [MonotonicFrameClock] with a time source controlled by a `kotlinx-coroutines-test`
+ * [DelayController]. This frame clock may be used to consistently drive time under controlled
+ * tests.
+ *
+ * Calls to [withFrameNanos] will schedule an upcoming frame [frameDelayNanos] nanoseconds in the
+ * future by launching into [coroutineScope] if such a frame has not yet been scheduled. The
+ * current frame time for [withFrameNanos] is provided by [delayController]. It is strongly
+ * suggested that [coroutineScope] contain the test dispatcher controlled by [delayController].
+ */
+@ExperimentalCoroutinesApi
+class TestMonotonicFrameClock(
+    private val coroutineScope: CoroutineScope,
+    private val delayController: DelayController,
+    @get:Suppress("MethodNameUnits") // Nanos for high-precision animation clocks
+    val frameDelayNanos: Long = DefaultFrameDelay
+) : MonotonicFrameClock {
+    private val lock = Any()
+    private val awaiters = mutableListOf<Awaiter<*>>()
+    private var posted = false
+
+    private class Awaiter<R>(
+        private val onFrame: (Long) -> R,
+        private val continuation: CancellableContinuation<R>
+    ) {
+        fun runFrame(frameTimeNanos: Long): () -> Unit {
+            val result = runCatching { onFrame(frameTimeNanos) }
+            return { continuation.resumeWith(result) }
+        }
+    }
+
+    override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R =
+        suspendCancellableCoroutine { co ->
+            synchronized(lock) {
+                awaiters.add(Awaiter(onFrame, co))
+                maybeLaunchTickRunner()
+            }
+        }
+
+    private fun maybeLaunchTickRunner() {
+        if (!posted) {
+            posted = true
+            coroutineScope.launch {
+                delay(frameDelayMillis)
+                synchronized(lock) {
+                    posted = false
+                    val toRun = awaiters.toList()
+                    awaiters.clear()
+                    val frameTime = delayController.currentTime * 1_000_000
+                    // In case of awaiters on an immediate dispatcher, run all frame callbacks
+                    // before resuming any associated continuations with the results.
+                    toRun.map { it.runFrame(frameTime) }.forEach { it() }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * The frame delay time for the [TestMonotonicFrameClock] in milliseconds.
+ */
+@ExperimentalCoroutinesApi
+val TestMonotonicFrameClock.frameDelayMillis: Long
+    get() = frameDelayNanos / 1_000_000
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
index 535c550..ae08758 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
@@ -33,7 +33,11 @@
     var onEndChangesCalled = 0
         private set
 
-    override fun insert(index: Int, instance: View) {
+    override fun insertTopDown(index: Int, instance: View) {
+        // Ignored as the tree is built bottom-up.
+    }
+
+    override fun insertBottomUp(index: Int, instance: View) {
         current.addAt(index, instance)
     }
 
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
index a6f913d..87326b1 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
@@ -34,7 +34,7 @@
 import androidx.compose.runtime.Recomposer
 import androidx.compose.runtime.dispatch.MonotonicFrameClock
 import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.test.TestMonotonicFrameClock
 import androidx.compose.ui.test.frameDelayMillis
@@ -134,7 +134,7 @@
         }
 
         composition = activity.setContent(recomposer) { testCase!!.Content() }
-        view = findAndroidOwner(activity)!!.view
+        view = findViewRootForTest(activity)!!.view
         @OptIn(ExperimentalComposeApi::class)
         Snapshot.notifyObjectsInitialized()
         simulationState = SimulationState.EmitContentDone
@@ -251,6 +251,9 @@
 
         composition?.dispose()
 
+        // Dispatcher will clean up the cancelled coroutines when it advances to them
+        testCoroutineDispatcher.advanceUntilIdle()
+
         // Clear the view
         val rootView = activity.findViewById(android.R.id.content) as ViewGroup
         rootView.removeAllViews()
@@ -305,18 +308,18 @@
     RecomposeDone
 }
 
-private fun findAndroidOwner(activity: Activity): AndroidOwner? {
-    return findAndroidOwner(activity.findViewById(android.R.id.content) as ViewGroup)
+private fun findViewRootForTest(activity: Activity): ViewRootForTest? {
+    return findViewRootForTest(activity.findViewById(android.R.id.content) as ViewGroup)
 }
 
-private fun findAndroidOwner(view: View): AndroidOwner? {
-    if (view is AndroidOwner) {
+private fun findViewRootForTest(view: View): ViewRootForTest? {
+    if (view is ViewRootForTest) {
         return view
     }
 
     if (view is ViewGroup) {
         for (i in 0 until view.childCount) {
-            val composeView = findAndroidOwner(view.getChildAt(i))
+            val composeView = findViewRootForTest(view.getChildAt(i))
             if (composeView != null) {
                 return composeView
             }
diff --git a/compose/ui/ui-geometry/api/current.txt b/compose/ui/ui-geometry/api/current.txt
index 750bfb4..02b89e631 100644
--- a/compose/ui/ui-geometry/api/current.txt
+++ b/compose/ui/ui-geometry/api/current.txt
@@ -99,6 +99,9 @@
 
   public final class OffsetKt {
     method @androidx.compose.runtime.Stable public static inline long Offset(float x, float y);
+    method public static boolean isFinite-k-4lQ0M(long);
+    method public static boolean isSpecified-k-4lQ0M(long);
+    method public static inline boolean isUnspecified-k-4lQ0M(long);
     method @androidx.compose.runtime.Stable public static long lerp-tX6QBWo(long start, long stop, float fraction);
   }
 
diff --git a/compose/ui/ui-geometry/api/public_plus_experimental_current.txt b/compose/ui/ui-geometry/api/public_plus_experimental_current.txt
index 750bfb4..02b89e631 100644
--- a/compose/ui/ui-geometry/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-geometry/api/public_plus_experimental_current.txt
@@ -99,6 +99,9 @@
 
   public final class OffsetKt {
     method @androidx.compose.runtime.Stable public static inline long Offset(float x, float y);
+    method public static boolean isFinite-k-4lQ0M(long);
+    method public static boolean isSpecified-k-4lQ0M(long);
+    method public static inline boolean isUnspecified-k-4lQ0M(long);
     method @androidx.compose.runtime.Stable public static long lerp-tX6QBWo(long start, long stop, float fraction);
   }
 
diff --git a/compose/ui/ui-geometry/api/restricted_current.txt b/compose/ui/ui-geometry/api/restricted_current.txt
index 750bfb4..02b89e631 100644
--- a/compose/ui/ui-geometry/api/restricted_current.txt
+++ b/compose/ui/ui-geometry/api/restricted_current.txt
@@ -99,6 +99,9 @@
 
   public final class OffsetKt {
     method @androidx.compose.runtime.Stable public static inline long Offset(float x, float y);
+    method public static boolean isFinite-k-4lQ0M(long);
+    method public static boolean isSpecified-k-4lQ0M(long);
+    method public static inline boolean isUnspecified-k-4lQ0M(long);
     method @androidx.compose.runtime.Stable public static long lerp-tX6QBWo(long start, long stop, float fraction);
   }
 
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
index ab1beaa..e497027 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
@@ -64,11 +64,23 @@
 
     @Stable
     val x: Float
-        get() = unpackFloat1(packedValue)
+        get() {
+            // Explicitly compare against packed values to avoid auto-boxing of Size.Unspecified
+            check(this.packedValue != Size.Unspecified.packedValue) {
+                "Offset is unspecified"
+            }
+            return unpackFloat1(packedValue)
+        }
 
     @Stable
     val y: Float
-        get() = unpackFloat2(packedValue)
+        get() {
+            // Explicitly compare against packed values to avoid auto-boxing of Size.Unspecified
+            check(this.packedValue != Size.Unspecified.packedValue) {
+                "Offset is unspecified"
+            }
+            return unpackFloat2(packedValue)
+        }
 
     @Stable
     operator fun component1(): Float = x
@@ -221,4 +233,22 @@
         lerp(start.x, stop.x, fraction),
         lerp(start.y, stop.y, fraction)
     )
-}
\ No newline at end of file
+}
+
+/**
+ * True if both x and y values of the [Offset] are finite
+ */
+@Stable
+val Offset.isFinite: Boolean get() = x.isFinite() && y.isFinite()
+
+/**
+ * `false` when this is [Offset.Unspecified].
+ */
+@Stable
+val Offset.isSpecified: Boolean get() = packedValue != Offset.Unspecified.packedValue
+
+/**
+ * `true` when this is [Offset.Unspecified].
+ */
+@Stable
+inline val Offset.isUnspecified: Boolean get() = packedValue == Offset.Unspecified.packedValue
\ No newline at end of file
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/OffsetTest.kt b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
index 82eb717..99b1de3 100644
--- a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
+++ b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/OffsetTest.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.geometry
 
 import org.junit.Assert
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -44,4 +46,64 @@
         Assert.assertEquals(100f, copy.x)
         Assert.assertEquals(300f, copy.y)
     }
+
+    @Test
+    fun testUnspecifiedWidthQueryThrows() {
+        try {
+            Offset.Unspecified.x
+            Assert.fail("Offset.Unspecified.x is not allowed")
+        } catch (t: Throwable) {
+            // no-op
+        }
+    }
+
+    @Test
+    fun testUnspecifiedHeightQueryThrows() {
+        try {
+            Offset.Unspecified.y
+            Assert.fail("Offset.Unspecified.y is not allowed")
+        } catch (t: Throwable) {
+            // no-op
+        }
+    }
+
+    @Test
+    fun testUnspecifiedCopyThrows() {
+        try {
+            Offset.Unspecified.copy(x = 100f)
+            Offset.Unspecified.copy(y = 70f)
+            Assert.fail("Offset.Unspecified.copy is not allowed")
+        } catch (t: Throwable) {
+            // no-op
+        }
+    }
+
+    @Test
+    fun testUnspecifiedComponentAssignmentThrows() {
+        try {
+            val (_, _) = Offset.Unspecified
+            Assert.fail("Size.Unspecified component assignment is not allowed")
+        } catch (t: Throwable) {
+            // no-op
+        }
+    }
+
+    @Test
+    fun testIsSpecified() {
+        val offset = Offset(10f, 20f)
+        assertTrue(offset.isSpecified)
+        assertFalse(offset.isUnspecified)
+    }
+
+    @Test
+    fun testIsUnspecified() {
+        assertTrue(Offset.Unspecified.isUnspecified)
+        assertFalse(Offset.Unspecified.isSpecified)
+    }
+
+    @Test
+    fun testUnspecifiedEquals() {
+        // Verify that verifying equality here does not crash
+        assertTrue(Offset.Unspecified == Offset.Unspecified)
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 50d3a44..51b2d1e 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -31,6 +31,7 @@
     method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
     method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
     method public android.graphics.PathEffect? getNativePathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public android.graphics.Shader? getShader();
     method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
     method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -45,6 +46,7 @@
     method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? value);
     method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality value);
     method public void setNativePathEffect(android.graphics.PathEffect? value);
+    method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? value);
     method public void setShader(android.graphics.Shader? value);
     method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap value);
     method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin value);
@@ -58,6 +60,7 @@
     property public androidx.compose.ui.graphics.FilterQuality filterQuality;
     property public boolean isAntiAlias;
     property public android.graphics.PathEffect? nativePathEffect;
+    property public androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public android.graphics.Shader? shader;
     property public androidx.compose.ui.graphics.StrokeCap strokeCap;
     property public androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -104,6 +107,11 @@
     property public boolean isEmpty;
   }
 
+  public final class AndroidPathEffectKt {
+    method public static android.graphics.PathEffect asAndroidPathEffect(androidx.compose.ui.graphics.PathEffect);
+    method public static androidx.compose.ui.graphics.PathEffect toComposePathEffect(android.graphics.PathEffect);
+  }
+
   public final class AndroidPathKt {
     method public static androidx.compose.ui.graphics.Path Path();
     method public static inline android.graphics.Path asAndroidPath(androidx.compose.ui.graphics.Path);
@@ -165,20 +173,36 @@
   }
 
   @androidx.compose.runtime.Immutable public abstract sealed class Brush {
-    method public abstract void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method @Deprecated public void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method public abstract void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+    field public static final androidx.compose.ui.graphics.Brush.Companion Companion;
+  }
+
+  public static final class Brush.Companion {
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush horizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, optional float startX, optional float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush horizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional float startX, optional float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush linearGradient-7_sGemo(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long start, optional long end, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush linearGradient-K4jYFb0(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long start, optional long end, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush radialGradient-YU3LRu0(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long center, optional float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush radialGradient-g04MWJE(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long center, optional float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush sweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long center);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush sweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long center);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush verticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, optional float startY, optional float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush verticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional float startY, optional float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
   }
 
   public final class BrushKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, long center);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, long center);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method public static androidx.compose.ui.graphics.ShaderBrush ShaderBrush(android.graphics.Shader shader);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, long center);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, long center);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
   }
 
   public interface Canvas {
@@ -358,6 +382,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class LinearGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final inline class Matrix {
@@ -430,7 +455,8 @@
     method public long getColor-0d7_KjU();
     method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
     method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
-    method public android.graphics.PathEffect? getNativePathEffect();
+    method @Deprecated public android.graphics.PathEffect? getNativePathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public android.graphics.Shader? getShader();
     method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
     method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -444,7 +470,8 @@
     method public void setColor-8_81llA(long p);
     method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? p);
     method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality p);
-    method public void setNativePathEffect(android.graphics.PathEffect? p);
+    method @Deprecated public void setNativePathEffect(android.graphics.PathEffect? p);
+    method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? p);
     method public void setShader(android.graphics.Shader? p);
     method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap p);
     method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin p);
@@ -457,7 +484,8 @@
     property public abstract androidx.compose.ui.graphics.ColorFilter? colorFilter;
     property public abstract androidx.compose.ui.graphics.FilterQuality filterQuality;
     property public abstract boolean isAntiAlias;
-    property public abstract android.graphics.PathEffect? nativePathEffect;
+    property @Deprecated public abstract android.graphics.PathEffect? nativePathEffect;
+    property public abstract androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public abstract android.graphics.Shader? shader;
     property public abstract androidx.compose.ui.graphics.StrokeCap strokeCap;
     property public abstract androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -511,6 +539,17 @@
     method public androidx.compose.ui.graphics.Path combine(androidx.compose.ui.graphics.PathOperation operation, androidx.compose.ui.graphics.Path path1, androidx.compose.ui.graphics.Path path2);
   }
 
+  public interface PathEffect {
+    field public static final androidx.compose.ui.graphics.PathEffect.Companion Companion;
+  }
+
+  public static final class PathEffect.Companion {
+    method public androidx.compose.ui.graphics.PathEffect chainPathEffect(androidx.compose.ui.graphics.PathEffect outer, androidx.compose.ui.graphics.PathEffect inner);
+    method public androidx.compose.ui.graphics.PathEffect cornerPathEffect(float radius);
+    method public androidx.compose.ui.graphics.PathEffect dashPathEffect(float[] intervals, optional float phase);
+    method public androidx.compose.ui.graphics.PathEffect stampedPathEffect(androidx.compose.ui.graphics.Path shape, float advance, float phase, androidx.compose.ui.graphics.StampedPathEffectStyle style);
+  }
+
   public enum PathFillType {
     enum_constant public static final androidx.compose.ui.graphics.PathFillType EvenOdd;
     enum_constant public static final androidx.compose.ui.graphics.PathFillType NonZero;
@@ -553,6 +592,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class RadialGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final class RectHelperKt {
@@ -565,11 +605,10 @@
     method public static androidx.compose.ui.graphics.Shape getRectangleShape();
   }
 
-  @androidx.compose.runtime.Immutable public class ShaderBrush extends androidx.compose.ui.graphics.Brush {
-    ctor public ShaderBrush(android.graphics.Shader shader);
-    method public final void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
-    method public final android.graphics.Shader getShader();
-    property public final android.graphics.Shader shader;
+  @androidx.compose.runtime.Immutable public abstract class ShaderBrush extends androidx.compose.ui.graphics.Brush {
+    ctor public ShaderBrush();
+    method public final void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+    method public abstract android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final class ShaderKt {
@@ -607,11 +646,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SolidColor extends androidx.compose.ui.graphics.Brush {
-    method public void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method public void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
     method public long getValue-0d7_KjU();
     property public final long value;
   }
 
+  public enum StampedPathEffectStyle {
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Morph;
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Rotate;
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Translate;
+  }
+
   public enum StrokeCap {
     enum_constant public static final androidx.compose.ui.graphics.StrokeCap Butt;
     enum_constant public static final androidx.compose.ui.graphics.StrokeCap Round;
@@ -625,6 +670,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SweepGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public enum TileMode {
@@ -860,14 +906,14 @@
     method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-1-s4MmQ(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-QXZmVdc(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRoundRect-fNghmuc(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
@@ -885,6 +931,10 @@
   public final class CanvasDrawScopeKt {
   }
 
+  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+    method public void drawContent();
+  }
+
   public interface DrawContext {
     method public androidx.compose.ui.graphics.Canvas getCanvas();
     method public long getSize-NH-jbRc();
@@ -902,14 +952,14 @@
     method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-1-s4MmQ(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-QXZmVdc(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRoundRect-fNghmuc(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
@@ -978,23 +1028,23 @@
   }
 
   public final class Stroke extends androidx.compose.ui.graphics.drawscope.DrawStyle {
-    ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+    ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
     ctor public Stroke();
     method public float component1();
     method public float component2();
     method public androidx.compose.ui.graphics.StrokeCap component3();
     method public androidx.compose.ui.graphics.StrokeJoin component4();
-    method public android.graphics.PathEffect? component5();
-    method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+    method public androidx.compose.ui.graphics.PathEffect? component5();
+    method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
     method public androidx.compose.ui.graphics.StrokeCap getCap();
     method public androidx.compose.ui.graphics.StrokeJoin getJoin();
     method public float getMiter();
-    method public android.graphics.PathEffect? getPathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public float getWidth();
     property public final androidx.compose.ui.graphics.StrokeCap cap;
     property public final androidx.compose.ui.graphics.StrokeJoin join;
     property public final float miter;
-    property public final android.graphics.PathEffect? pathEffect;
+    property public final androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public final float width;
     field public static final androidx.compose.ui.graphics.drawscope.Stroke.Companion Companion;
     field public static final float DefaultMiter = 4.0f;
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 50d3a44..51b2d1e 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -31,6 +31,7 @@
     method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
     method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
     method public android.graphics.PathEffect? getNativePathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public android.graphics.Shader? getShader();
     method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
     method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -45,6 +46,7 @@
     method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? value);
     method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality value);
     method public void setNativePathEffect(android.graphics.PathEffect? value);
+    method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? value);
     method public void setShader(android.graphics.Shader? value);
     method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap value);
     method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin value);
@@ -58,6 +60,7 @@
     property public androidx.compose.ui.graphics.FilterQuality filterQuality;
     property public boolean isAntiAlias;
     property public android.graphics.PathEffect? nativePathEffect;
+    property public androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public android.graphics.Shader? shader;
     property public androidx.compose.ui.graphics.StrokeCap strokeCap;
     property public androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -104,6 +107,11 @@
     property public boolean isEmpty;
   }
 
+  public final class AndroidPathEffectKt {
+    method public static android.graphics.PathEffect asAndroidPathEffect(androidx.compose.ui.graphics.PathEffect);
+    method public static androidx.compose.ui.graphics.PathEffect toComposePathEffect(android.graphics.PathEffect);
+  }
+
   public final class AndroidPathKt {
     method public static androidx.compose.ui.graphics.Path Path();
     method public static inline android.graphics.Path asAndroidPath(androidx.compose.ui.graphics.Path);
@@ -165,20 +173,36 @@
   }
 
   @androidx.compose.runtime.Immutable public abstract sealed class Brush {
-    method public abstract void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method @Deprecated public void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method public abstract void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+    field public static final androidx.compose.ui.graphics.Brush.Companion Companion;
+  }
+
+  public static final class Brush.Companion {
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush horizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, optional float startX, optional float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush horizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional float startX, optional float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush linearGradient-7_sGemo(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long start, optional long end, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush linearGradient-K4jYFb0(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long start, optional long end, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush radialGradient-YU3LRu0(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long center, optional float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush radialGradient-g04MWJE(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long center, optional float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush sweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long center);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush sweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long center);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush verticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, optional float startY, optional float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush verticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional float startY, optional float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
   }
 
   public final class BrushKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, long center);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, long center);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method public static androidx.compose.ui.graphics.ShaderBrush ShaderBrush(android.graphics.Shader shader);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, long center);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, long center);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
   }
 
   public interface Canvas {
@@ -358,6 +382,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class LinearGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final inline class Matrix {
@@ -430,7 +455,8 @@
     method public long getColor-0d7_KjU();
     method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
     method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
-    method public android.graphics.PathEffect? getNativePathEffect();
+    method @Deprecated public android.graphics.PathEffect? getNativePathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public android.graphics.Shader? getShader();
     method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
     method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -444,7 +470,8 @@
     method public void setColor-8_81llA(long p);
     method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? p);
     method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality p);
-    method public void setNativePathEffect(android.graphics.PathEffect? p);
+    method @Deprecated public void setNativePathEffect(android.graphics.PathEffect? p);
+    method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? p);
     method public void setShader(android.graphics.Shader? p);
     method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap p);
     method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin p);
@@ -457,7 +484,8 @@
     property public abstract androidx.compose.ui.graphics.ColorFilter? colorFilter;
     property public abstract androidx.compose.ui.graphics.FilterQuality filterQuality;
     property public abstract boolean isAntiAlias;
-    property public abstract android.graphics.PathEffect? nativePathEffect;
+    property @Deprecated public abstract android.graphics.PathEffect? nativePathEffect;
+    property public abstract androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public abstract android.graphics.Shader? shader;
     property public abstract androidx.compose.ui.graphics.StrokeCap strokeCap;
     property public abstract androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -511,6 +539,17 @@
     method public androidx.compose.ui.graphics.Path combine(androidx.compose.ui.graphics.PathOperation operation, androidx.compose.ui.graphics.Path path1, androidx.compose.ui.graphics.Path path2);
   }
 
+  public interface PathEffect {
+    field public static final androidx.compose.ui.graphics.PathEffect.Companion Companion;
+  }
+
+  public static final class PathEffect.Companion {
+    method public androidx.compose.ui.graphics.PathEffect chainPathEffect(androidx.compose.ui.graphics.PathEffect outer, androidx.compose.ui.graphics.PathEffect inner);
+    method public androidx.compose.ui.graphics.PathEffect cornerPathEffect(float radius);
+    method public androidx.compose.ui.graphics.PathEffect dashPathEffect(float[] intervals, optional float phase);
+    method public androidx.compose.ui.graphics.PathEffect stampedPathEffect(androidx.compose.ui.graphics.Path shape, float advance, float phase, androidx.compose.ui.graphics.StampedPathEffectStyle style);
+  }
+
   public enum PathFillType {
     enum_constant public static final androidx.compose.ui.graphics.PathFillType EvenOdd;
     enum_constant public static final androidx.compose.ui.graphics.PathFillType NonZero;
@@ -553,6 +592,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class RadialGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final class RectHelperKt {
@@ -565,11 +605,10 @@
     method public static androidx.compose.ui.graphics.Shape getRectangleShape();
   }
 
-  @androidx.compose.runtime.Immutable public class ShaderBrush extends androidx.compose.ui.graphics.Brush {
-    ctor public ShaderBrush(android.graphics.Shader shader);
-    method public final void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
-    method public final android.graphics.Shader getShader();
-    property public final android.graphics.Shader shader;
+  @androidx.compose.runtime.Immutable public abstract class ShaderBrush extends androidx.compose.ui.graphics.Brush {
+    ctor public ShaderBrush();
+    method public final void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+    method public abstract android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final class ShaderKt {
@@ -607,11 +646,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SolidColor extends androidx.compose.ui.graphics.Brush {
-    method public void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method public void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
     method public long getValue-0d7_KjU();
     property public final long value;
   }
 
+  public enum StampedPathEffectStyle {
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Morph;
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Rotate;
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Translate;
+  }
+
   public enum StrokeCap {
     enum_constant public static final androidx.compose.ui.graphics.StrokeCap Butt;
     enum_constant public static final androidx.compose.ui.graphics.StrokeCap Round;
@@ -625,6 +670,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SweepGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public enum TileMode {
@@ -860,14 +906,14 @@
     method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-1-s4MmQ(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-QXZmVdc(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRoundRect-fNghmuc(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
@@ -885,6 +931,10 @@
   public final class CanvasDrawScopeKt {
   }
 
+  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+    method public void drawContent();
+  }
+
   public interface DrawContext {
     method public androidx.compose.ui.graphics.Canvas getCanvas();
     method public long getSize-NH-jbRc();
@@ -902,14 +952,14 @@
     method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-1-s4MmQ(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-QXZmVdc(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRoundRect-fNghmuc(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
@@ -978,23 +1028,23 @@
   }
 
   public final class Stroke extends androidx.compose.ui.graphics.drawscope.DrawStyle {
-    ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+    ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
     ctor public Stroke();
     method public float component1();
     method public float component2();
     method public androidx.compose.ui.graphics.StrokeCap component3();
     method public androidx.compose.ui.graphics.StrokeJoin component4();
-    method public android.graphics.PathEffect? component5();
-    method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+    method public androidx.compose.ui.graphics.PathEffect? component5();
+    method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
     method public androidx.compose.ui.graphics.StrokeCap getCap();
     method public androidx.compose.ui.graphics.StrokeJoin getJoin();
     method public float getMiter();
-    method public android.graphics.PathEffect? getPathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public float getWidth();
     property public final androidx.compose.ui.graphics.StrokeCap cap;
     property public final androidx.compose.ui.graphics.StrokeJoin join;
     property public final float miter;
-    property public final android.graphics.PathEffect? pathEffect;
+    property public final androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public final float width;
     field public static final androidx.compose.ui.graphics.drawscope.Stroke.Companion Companion;
     field public static final float DefaultMiter = 4.0f;
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index f7b2958..7aae932 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -61,6 +61,7 @@
     method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
     method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
     method public android.graphics.PathEffect? getNativePathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public android.graphics.Shader? getShader();
     method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
     method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -75,6 +76,7 @@
     method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? value);
     method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality value);
     method public void setNativePathEffect(android.graphics.PathEffect? value);
+    method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? value);
     method public void setShader(android.graphics.Shader? value);
     method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap value);
     method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin value);
@@ -88,6 +90,7 @@
     property public androidx.compose.ui.graphics.FilterQuality filterQuality;
     property public boolean isAntiAlias;
     property public android.graphics.PathEffect? nativePathEffect;
+    property public androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public android.graphics.Shader? shader;
     property public androidx.compose.ui.graphics.StrokeCap strokeCap;
     property public androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -134,6 +137,11 @@
     property public boolean isEmpty;
   }
 
+  public final class AndroidPathEffectKt {
+    method public static android.graphics.PathEffect asAndroidPathEffect(androidx.compose.ui.graphics.PathEffect);
+    method public static androidx.compose.ui.graphics.PathEffect toComposePathEffect(android.graphics.PathEffect);
+  }
+
   public final class AndroidPathKt {
     method public static androidx.compose.ui.graphics.Path Path();
     method public static inline android.graphics.Path asAndroidPath(androidx.compose.ui.graphics.Path);
@@ -195,20 +203,36 @@
   }
 
   @androidx.compose.runtime.Immutable public abstract sealed class Brush {
-    method public abstract void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method @Deprecated public void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method public abstract void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+    field public static final androidx.compose.ui.graphics.Brush.Companion Companion;
+  }
+
+  public static final class Brush.Companion {
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush horizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, optional float startX, optional float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush horizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional float startX, optional float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush linearGradient-7_sGemo(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long start, optional long end, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush linearGradient-K4jYFb0(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long start, optional long end, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush radialGradient-YU3LRu0(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long center, optional float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush radialGradient-g04MWJE(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long center, optional float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush sweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional long center);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush sweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, optional long center);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush verticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, optional float startY, optional float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.graphics.Brush verticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, optional float startY, optional float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
   }
 
   public final class BrushKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, long center);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, long center);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient HorizontalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float endX, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient LinearGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startX, float startY, float endX, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.RadialGradient RadialGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float centerX, float centerY, float radius, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method public static androidx.compose.ui.graphics.ShaderBrush ShaderBrush(android.graphics.Shader shader);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-PvDSl28(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, long center);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.SweepGradient SweepGradient-acbAMd8(java.util.List<androidx.compose.ui.graphics.Color> colors, long center);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(java.util.List<androidx.compose.ui.graphics.Color> colors, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.graphics.LinearGradient VerticalGradient(kotlin.Pair<java.lang.Float,androidx.compose.ui.graphics.Color>![] colorStops, float startY, float endY, optional androidx.compose.ui.graphics.TileMode tileMode);
   }
 
   public interface Canvas {
@@ -390,6 +414,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class LinearGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final inline class Matrix {
@@ -462,7 +487,8 @@
     method public long getColor-0d7_KjU();
     method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
     method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
-    method public android.graphics.PathEffect? getNativePathEffect();
+    method @Deprecated public android.graphics.PathEffect? getNativePathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public android.graphics.Shader? getShader();
     method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
     method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -476,7 +502,8 @@
     method public void setColor-8_81llA(long p);
     method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? p);
     method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality p);
-    method public void setNativePathEffect(android.graphics.PathEffect? p);
+    method @Deprecated public void setNativePathEffect(android.graphics.PathEffect? p);
+    method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? p);
     method public void setShader(android.graphics.Shader? p);
     method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap p);
     method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin p);
@@ -489,7 +516,8 @@
     property public abstract androidx.compose.ui.graphics.ColorFilter? colorFilter;
     property public abstract androidx.compose.ui.graphics.FilterQuality filterQuality;
     property public abstract boolean isAntiAlias;
-    property public abstract android.graphics.PathEffect? nativePathEffect;
+    property @Deprecated public abstract android.graphics.PathEffect? nativePathEffect;
+    property public abstract androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public abstract android.graphics.Shader? shader;
     property public abstract androidx.compose.ui.graphics.StrokeCap strokeCap;
     property public abstract androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -543,6 +571,17 @@
     method public androidx.compose.ui.graphics.Path combine(androidx.compose.ui.graphics.PathOperation operation, androidx.compose.ui.graphics.Path path1, androidx.compose.ui.graphics.Path path2);
   }
 
+  public interface PathEffect {
+    field public static final androidx.compose.ui.graphics.PathEffect.Companion Companion;
+  }
+
+  public static final class PathEffect.Companion {
+    method public androidx.compose.ui.graphics.PathEffect chainPathEffect(androidx.compose.ui.graphics.PathEffect outer, androidx.compose.ui.graphics.PathEffect inner);
+    method public androidx.compose.ui.graphics.PathEffect cornerPathEffect(float radius);
+    method public androidx.compose.ui.graphics.PathEffect dashPathEffect(float[] intervals, optional float phase);
+    method public androidx.compose.ui.graphics.PathEffect stampedPathEffect(androidx.compose.ui.graphics.Path shape, float advance, float phase, androidx.compose.ui.graphics.StampedPathEffectStyle style);
+  }
+
   public enum PathFillType {
     enum_constant public static final androidx.compose.ui.graphics.PathFillType EvenOdd;
     enum_constant public static final androidx.compose.ui.graphics.PathFillType NonZero;
@@ -585,6 +624,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class RadialGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final class RectHelperKt {
@@ -597,11 +637,10 @@
     method public static androidx.compose.ui.graphics.Shape getRectangleShape();
   }
 
-  @androidx.compose.runtime.Immutable public class ShaderBrush extends androidx.compose.ui.graphics.Brush {
-    ctor public ShaderBrush(android.graphics.Shader shader);
-    method public final void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
-    method public final android.graphics.Shader getShader();
-    property public final android.graphics.Shader shader;
+  @androidx.compose.runtime.Immutable public abstract class ShaderBrush extends androidx.compose.ui.graphics.Brush {
+    ctor public ShaderBrush();
+    method public final void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+    method public abstract android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public final class ShaderKt {
@@ -639,11 +678,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SolidColor extends androidx.compose.ui.graphics.Brush {
-    method public void applyTo(androidx.compose.ui.graphics.Paint p, float alpha);
+    method public void applyTo-TJof4Gw(long size, androidx.compose.ui.graphics.Paint p, float alpha);
     method public long getValue-0d7_KjU();
     property public final long value;
   }
 
+  public enum StampedPathEffectStyle {
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Morph;
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Rotate;
+    enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Translate;
+  }
+
   public enum StrokeCap {
     enum_constant public static final androidx.compose.ui.graphics.StrokeCap Butt;
     enum_constant public static final androidx.compose.ui.graphics.StrokeCap Round;
@@ -657,6 +702,7 @@
   }
 
   @androidx.compose.runtime.Immutable public final class SweepGradient extends androidx.compose.ui.graphics.ShaderBrush {
+    method public android.graphics.Shader createShader-uvyYCjk(long size);
   }
 
   public enum TileMode {
@@ -892,14 +938,14 @@
     method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-1-s4MmQ(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-QXZmVdc(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRoundRect-fNghmuc(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
@@ -941,6 +987,10 @@
   public final class CanvasDrawScopeKt {
   }
 
+  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+    method public void drawContent();
+  }
+
   public interface DrawContext {
     method public androidx.compose.ui.graphics.Canvas getCanvas();
     method public long getSize-NH-jbRc();
@@ -958,14 +1008,14 @@
     method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-1-s4MmQ(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-QXZmVdc(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawOval-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
-    method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+    method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRect-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
     method public void drawRoundRect-fNghmuc(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
@@ -1034,23 +1084,23 @@
   }
 
   public final class Stroke extends androidx.compose.ui.graphics.drawscope.DrawStyle {
-    ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+    ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
     ctor public Stroke();
     method public float component1();
     method public float component2();
     method public androidx.compose.ui.graphics.StrokeCap component3();
     method public androidx.compose.ui.graphics.StrokeJoin component4();
-    method public android.graphics.PathEffect? component5();
-    method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+    method public androidx.compose.ui.graphics.PathEffect? component5();
+    method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
     method public androidx.compose.ui.graphics.StrokeCap getCap();
     method public androidx.compose.ui.graphics.StrokeJoin getJoin();
     method public float getMiter();
-    method public android.graphics.PathEffect? getPathEffect();
+    method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
     method public float getWidth();
     property public final androidx.compose.ui.graphics.StrokeCap cap;
     property public final androidx.compose.ui.graphics.StrokeJoin join;
     property public final float miter;
-    property public final android.graphics.PathEffect? pathEffect;
+    property public final androidx.compose.ui.graphics.PathEffect? pathEffect;
     property public final float width;
     field public static final androidx.compose.ui.graphics.drawscope.Stroke.Companion Companion;
     field public static final float DefaultMiter = 4.0f;
diff --git a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
new file mode 100644
index 0000000..4784e83
--- /dev/null
+++ b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Sampled
+@Composable
+fun GradientBrushSample() {
+    Column(modifier = Modifier.fillMaxSize().wrapContentSize()) {
+
+        // Create a linear gradient that shows red in the top left corner
+        // and blue in the bottom right corner
+        val linear = Brush.linearGradient(listOf(Color.Red, Color.Blue))
+
+        Box(modifier = Modifier.size(120.dp).background(linear))
+
+        Spacer(modifier = Modifier.size(20.dp))
+
+        // Create a radial gradient centered about the drawing area that is green on
+        // the outer
+        // edge of the circle and magenta towards the center of the circle
+        val radial = Brush.radialGradient(listOf(Color.Green, Color.Magenta))
+        Box(modifier = Modifier.size(120.dp).background(radial))
+
+        Spacer(modifier = Modifier.size(20.dp))
+
+        // Create a radial gradient centered about the drawing area that is green on
+        // the outer
+        // edge of the circle and magenta towards the center of the circle
+        val sweep = Brush.sweepGradient(listOf(Color.Cyan, Color.Magenta))
+        Box(modifier = Modifier.size(120.dp).background(sweep))
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/PathEffectSample.kt b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/PathEffectSample.kt
new file mode 100644
index 0000000..09c5dcc
--- /dev/null
+++ b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/PathEffectSample.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.StampedPathEffectStyle
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.unit.dp
+
+@Sampled
+@Composable
+fun StampedPathEffectSample() {
+    val size = 20f
+    val square = Path().apply {
+        lineTo(size, 0f)
+        lineTo(size, size)
+        lineTo(0f, size)
+        close()
+    }
+    Column(modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center)) {
+        val canvasModifier = Modifier.size(80.dp).align(Alignment.CenterHorizontally)
+
+        // StampedPathEffectStyle.Morph will modify the lines of the square to be curved to fit
+        // the curvature of the circle itself. Each stamped square will be rendered as an arc
+        // that is fully contained by the bounds of the circle itself
+        Canvas(modifier = canvasModifier) {
+            drawCircle(color = Color.Blue)
+            drawCircle(
+                color = Color.Red,
+                style = Stroke(
+                    pathEffect = PathEffect.stampedPathEffect(
+                        shape = square,
+                        style = StampedPathEffectStyle.Morph,
+                        phase = 0f,
+                        advance = 30f
+                    )
+                )
+            )
+        }
+
+        Spacer(modifier = Modifier.size(10.dp))
+
+        // StampedPathEffectStyle.Rotate will draw the square repeatedly around the circle
+        // such that each stamped square is centered on the circumference of the circle and is
+        // rotated along the curvature of the circle itself
+        Canvas(modifier = canvasModifier) {
+            drawCircle(color = Color.Blue)
+            drawCircle(
+                color = Color.Red,
+                style = Stroke(
+                    pathEffect = PathEffect.stampedPathEffect(
+                        shape = square,
+                        style = StampedPathEffectStyle.Rotate,
+                        phase = 0f,
+                        advance = 30f
+                    )
+                )
+            )
+        }
+
+        Spacer(modifier = Modifier.size(10.dp))
+
+        // StampedPathEffectStyle.Translate will draw the square repeatedly around the circle
+        // with the top left of each stamped square on the circumference of the circle
+        Canvas(modifier = canvasModifier) {
+            drawCircle(color = Color.Blue)
+            drawCircle(
+                color = Color.Red,
+                style = Stroke(
+                    pathEffect = PathEffect.stampedPathEffect(
+                        shape = square,
+                        style = StampedPathEffectStyle.Translate,
+                        phase = 0f,
+                        advance = 30f
+                    )
+                )
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
index ecec048..c5bb9d6 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
@@ -17,8 +17,8 @@
 package androidx.compose.ui.graphics
 
 import android.content.Context
+import android.graphics.Bitmap
 import android.graphics.Canvas
-import android.graphics.Color
 import android.os.Build
 import android.util.AttributeSet
 import android.view.Gravity
@@ -61,14 +61,14 @@
         activityTestRule.runOnUiThread {
             val group = EnableDisableZViewGroup(drawLatch, activity)
             groupView = group
-            group.setBackgroundColor(Color.WHITE)
+            group.setBackgroundColor(android.graphics.Color.WHITE)
             group.layoutParams = ViewGroup.LayoutParams(12, 12)
             val child = View(activity)
             val childLayoutParams = FrameLayout.LayoutParams(10, 10)
             childLayoutParams.gravity = Gravity.TOP or Gravity.LEFT
             child.layoutParams = childLayoutParams
             child.elevation = 4f
-            child.setBackgroundColor(Color.WHITE)
+            child.setBackgroundColor(android.graphics.Color.WHITE)
             group.addView(child)
             activity.setContentView(group)
         }
@@ -78,9 +78,9 @@
         // the drawn content can get onto the screen before we capture the bitmap.
         activityTestRule.runOnUiThread { }
         val bitmap = groupView!!.captureToImage().asAndroidBitmap()
-        assertEquals(Color.WHITE, bitmap.getPixel(0, 0))
-        assertEquals(Color.WHITE, bitmap.getPixel(9, 9))
-        assertNotEquals(Color.WHITE, bitmap.getPixel(10, 10))
+        assertEquals(android.graphics.Color.WHITE, bitmap.getPixel(0, 0))
+        assertEquals(android.graphics.Color.WHITE, bitmap.getPixel(9, 9))
+        assertNotEquals(android.graphics.Color.WHITE, bitmap.getPixel(10, 10))
     }
 
     @Test
@@ -266,6 +266,215 @@
         assertEquals(bg, pixelMap[75, 76])
     }
 
+    @Test
+    fun testCornerPathEffect() {
+        val width = 80
+        val height = 80
+        val radius = 20f
+        val imageBitmap = ImageBitmap(width, height)
+        imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+        val canvas = Canvas(imageBitmap)
+        canvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            Paint().apply {
+                color = Color.Blue
+                pathEffect = PathEffect.cornerPathEffect(radius)
+            }
+        )
+
+        val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+        androidBitmap.eraseColor(android.graphics.Color.WHITE)
+        val androidCanvas = android.graphics.Canvas(androidBitmap)
+        androidCanvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            android.graphics.Paint().apply {
+                isAntiAlias = true
+                color = android.graphics.Color.BLUE
+                pathEffect = android.graphics.CornerPathEffect(radius)
+            }
+        )
+
+        val composePixels = imageBitmap.toPixelMap()
+        for (i in 0 until 80) {
+            for (j in 0 until 80) {
+                assertEquals(
+                    "invalid color at i: " + i + ", " + j,
+                    composePixels[i, j].toArgb(),
+                    androidBitmap.getPixel(i, j)
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testDashPathEffect() {
+        val width = 80
+        val height = 80
+        val imageBitmap = ImageBitmap(width, height)
+        imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+        val canvas = Canvas(imageBitmap)
+        canvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            Paint().apply {
+                color = Color.Blue
+                pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 8f)
+            }
+        )
+
+        val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+        androidBitmap.eraseColor(android.graphics.Color.WHITE)
+        val androidCanvas = android.graphics.Canvas(androidBitmap)
+        androidCanvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            android.graphics.Paint().apply {
+                isAntiAlias = true
+                color = android.graphics.Color.BLUE
+                pathEffect = android.graphics.DashPathEffect(floatArrayOf(10f, 5f), 8f)
+            }
+        )
+
+        val composePixels = imageBitmap.toPixelMap()
+        for (i in 0 until 80) {
+            for (j in 0 until 80) {
+                assertEquals(
+                    "invalid color at i: " + i + ", " + j,
+                    composePixels[i, j].toArgb(),
+                    androidBitmap.getPixel(i, j)
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testChainPathEffect() {
+        val width = 80
+        val height = 80
+        val imageBitmap = ImageBitmap(width, height)
+        imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+        val canvas = Canvas(imageBitmap)
+        canvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            Paint().apply {
+                color = Color.Blue
+                pathEffect =
+                    PathEffect.chainPathEffect(
+                        PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 8f),
+                        PathEffect.cornerPathEffect(20f)
+                    )
+            }
+        )
+
+        val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+        androidBitmap.eraseColor(android.graphics.Color.WHITE)
+        val androidCanvas = android.graphics.Canvas(androidBitmap)
+        androidCanvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            android.graphics.Paint().apply {
+                isAntiAlias = true
+                color = android.graphics.Color.BLUE
+                pathEffect =
+                    android.graphics.ComposePathEffect(
+                        android.graphics.DashPathEffect(floatArrayOf(10f, 5f), 8f),
+                        android.graphics.CornerPathEffect(20f)
+                    )
+            }
+        )
+
+        val composePixels = imageBitmap.toPixelMap()
+        for (i in 0 until 80) {
+            for (j in 0 until 80) {
+                assertEquals(
+                    "invalid color at i: " + i + ", " + j,
+                    composePixels[i, j].toArgb(),
+                    androidBitmap.getPixel(i, j)
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testPathDashPathEffect() {
+        val width = 80
+        val height = 80
+        val imageBitmap = ImageBitmap(width, height)
+        imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+        val canvas = Canvas(imageBitmap)
+        canvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            Paint().apply {
+                color = Color.Blue
+                pathEffect =
+                    PathEffect.stampedPathEffect(
+                        Path().apply {
+                            lineTo(0f, 5f)
+                            lineTo(5f, 5f)
+                            close()
+                        },
+                        5f,
+                        2f,
+                        StampedPathEffectStyle.Rotate
+                    )
+            }
+        )
+
+        val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+        androidBitmap.eraseColor(android.graphics.Color.WHITE)
+        val androidCanvas = android.graphics.Canvas(androidBitmap)
+        androidCanvas.drawRect(
+            0f,
+            0f,
+            width.toFloat(),
+            height.toFloat(),
+            android.graphics.Paint().apply {
+                isAntiAlias = true
+                color = android.graphics.Color.BLUE
+                pathEffect =
+                    android.graphics.PathDashPathEffect(
+                        android.graphics.Path().apply {
+                            lineTo(0f, 5f)
+                            lineTo(5f, 5f)
+                            close()
+                        },
+                        5f,
+                        2f,
+                        android.graphics.PathDashPathEffect.Style.ROTATE
+                    )
+            }
+        )
+
+        val composePixels = imageBitmap.toPixelMap()
+        for (i in 0 until 80) {
+            for (j in 0 until 80) {
+                assertEquals(
+                    "invalid color at i: " + i + ", " + j,
+                    composePixels[i, j].toArgb(),
+                    androidBitmap.getPixel(i, j)
+                )
+            }
+        }
+    }
+
     class EnableDisableZViewGroup @JvmOverloads constructor(
         val drawLatch: CountDownLatch,
         context: Context,
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
index ec1b135..dd24cefd 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
@@ -38,15 +38,14 @@
         val imageBitmap = ImageBitmap(100, 100)
         imageBitmap.drawInto {
             drawRect(
-                brush = LinearGradient(
+                brush = Brush.linearGradient(
                     0.0f to Color.Red,
                     0.5f to Color.Red,
                     0.5f to Color.Blue,
                     1.0f to Color.Blue,
-                    startX = 0.0f,
-                    startY = 0.0f,
-                    endX = 0.0f,
-                    endY = 100f
+                    start = Offset.Zero,
+                    end = Offset(0.0f, 100f),
+                    tileMode = TileMode.Clamp
                 )
             )
         }
@@ -68,14 +67,14 @@
 
         imageBitmap.drawInto {
             drawCircle(
-                brush = RadialGradient(
+                brush = Brush.radialGradient(
                     0.0f to Color.Red,
                     0.5f to Color.Red,
                     0.5f to Color.Blue,
                     1.0f to Color.Blue,
-                    centerX = 50f,
-                    centerY = 50f,
-                    radius = 50f
+                    center = Offset(50f, 50f),
+                    radius = 50f,
+                    tileMode = TileMode.Clamp
                 )
             )
         }
@@ -100,12 +99,12 @@
         val center = Offset(50f, 50f)
         imageBitmap.drawInto {
             drawRect(
-                brush = SweepGradient(
+                brush = Brush.sweepGradient(
                     0.0f to Color.Red,
                     0.5f to Color.Red,
                     0.5f to Color.Blue,
                     1.0f to Color.Blue,
-                    center = center,
+                    center = center
                 )
             )
         }
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt
index b78b5f0..55730af 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt
@@ -19,15 +19,20 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.ClipOp
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.LinearGradientShader
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.PointMode
+import androidx.compose.ui.graphics.RadialGradientShader
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.SweepGradientShader
+import androidx.compose.ui.graphics.TileMode
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.unit.Density
@@ -972,6 +977,545 @@
         }
     }
 
+    @Test
+    fun testLinearGradient() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(Brush.linearGradient(listOf(Color.Red, Color.Green, Color.Blue)))
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset.Zero,
+                        Offset(100f, 100f),
+                        listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testLinearGradientBottomEnd() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.linearGradient(
+                        listOf(Color.Red, Color.Green, Color.Blue),
+                        end = Offset(0f, Float.POSITIVE_INFINITY)
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset.Zero,
+                        Offset(0f, 100f),
+                        listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testLinearGradientRightEnd() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.linearGradient(
+                        listOf(Color.Red, Color.Green, Color.Blue),
+                        end = Offset(Float.POSITIVE_INFINITY, 0f)
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset.Zero,
+                        Offset(100f, 0f),
+                        listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testLinearGradientBottomStart() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.linearGradient(
+                        listOf(Color.Red, Color.Green, Color.Blue),
+                        start = Offset(0f, Float.POSITIVE_INFINITY)
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset(0f, 100f),
+                        Offset(100f, 100f),
+                        listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testLinearGradientRightStart() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.linearGradient(
+                        listOf(Color.Red, Color.Green, Color.Blue),
+                        start = Offset(Float.POSITIVE_INFINITY, 0f)
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset(100f, 0f),
+                        Offset(100f, 100f),
+                        listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testLinearGradientWithStops() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.linearGradient(
+                        0.0f to Color.Red,
+                        0.1f to Color.Green,
+                        0.8f to Color.Blue,
+                        start = Offset(10.0f, 10f),
+                        tileMode = TileMode.Repeated
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset(10f, 10f),
+                        Offset(100f, 100f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue),
+                        colorStops = listOf(0.0f, 0.1f, 0.8f),
+                        tileMode = TileMode.Repeated
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testHorizontalGradient() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(Brush.horizontalGradient(listOf(Color.Red, Color.Green, Color.Blue)))
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset.Zero,
+                        Offset(100f, 0f),
+                        listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testHorizontalGradientWithStops() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.horizontalGradient(
+                        0.0f to Color.Red,
+                        0.1f to Color.Green,
+                        0.8f to Color.Blue,
+                        startX = 10f,
+                        tileMode = TileMode.Repeated
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset(10f, 0f),
+                        Offset(100f, 0f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue),
+                        colorStops = listOf(0.0f, 0.1f, 0.8f),
+                        tileMode = TileMode.Repeated
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testVerticalGradient() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(Brush.verticalGradient(listOf(Color.Red, Color.Green, Color.Blue)))
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset.Zero,
+                        Offset(0f, 100f),
+                        listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testVerticalGradientWithStops() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.verticalGradient(
+                        0.0f to Color.Red,
+                        0.1f to Color.Green,
+                        0.8f to Color.Blue,
+                        startY = 10f,
+                        tileMode = TileMode.Repeated
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = LinearGradientShader(
+                        Offset(0f, 10f),
+                        Offset(0f, 100f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue),
+                        colorStops = listOf(0.0f, 0.1f, 0.8f),
+                        tileMode = TileMode.Repeated
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testRadialGradient() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.radialGradient(listOf(Color.Red, Color.Green, Color.Blue))
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = RadialGradientShader(
+                        Offset(50f, 50f),
+                        50f,
+                        colors = listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testRadialGradientOutsideDrawingBounds() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                val offsetRadialGradient = Brush.radialGradient(
+                    listOf(Color.Red, Color.Blue),
+                    center = Offset(150f, 150f),
+                    radius = 50f
+                )
+                drawRect(offsetRadialGradient)
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = RadialGradientShader(
+                        Offset(150f, 150f),
+                        radius = 50f,
+                        colors = listOf(Color.Red, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testRadialGradientBottomRight() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                val offsetRadialGradient = Brush.radialGradient(
+                    listOf(Color.Red, Color.Blue),
+                    center = Offset.Infinite
+                )
+                drawRect(offsetRadialGradient)
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = RadialGradientShader(
+                        Offset(100f, 100f),
+                        radius = 50f,
+                        colors = listOf(Color.Red, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testRadialGradientRight() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                val offsetRadialGradient = Brush.radialGradient(
+                    listOf(Color.Red, Color.Blue),
+                    center = Offset(Float.POSITIVE_INFINITY, 0f)
+                )
+                drawRect(offsetRadialGradient)
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = RadialGradientShader(
+                        Offset(100f, 0f),
+                        radius = 50f,
+                        colors = listOf(Color.Red, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testRadialGradientBottom() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                val offsetRadialGradient = Brush.radialGradient(
+                    listOf(Color.Red, Color.Blue),
+                    center = Offset(0f, Float.POSITIVE_INFINITY)
+                )
+                drawRect(offsetRadialGradient)
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = RadialGradientShader(
+                        Offset(0f, 100f),
+                        radius = 50f,
+                        colors = listOf(Color.Red, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testRadialGradientWithStops() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.radialGradient(
+                        0.0f to Color.Red,
+                        0.1f to Color.Green,
+                        0.8f to Color.Blue,
+                        radius = 10f,
+                        tileMode = TileMode.Mirror
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = RadialGradientShader(
+                        Offset(50f, 50f),
+                        10f,
+                        colors = listOf(Color.Red, Color.Green, Color.Blue),
+                        colorStops = listOf(0.0f, 0.1f, 0.8f),
+                        tileMode = TileMode.Mirror
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testSweepGradient() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.sweepGradient(listOf(Color.Red, Color.Green, Color.Blue))
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = SweepGradientShader(
+                        Offset(50f, 50f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testSweepGradientBottomRight() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.sweepGradient(
+                        listOf(Color.Red, Color.Green, Color.Blue),
+                        center = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = SweepGradientShader(
+                        Offset(100f, 100f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testSweepGradientBottom() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.sweepGradient(
+                        listOf(Color.Red, Color.Green, Color.Blue),
+                        center = Offset(0f, Float.POSITIVE_INFINITY)
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = SweepGradientShader(
+                        Offset(0f, 100f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testSweepGradientRight() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.sweepGradient(
+                        listOf(Color.Red, Color.Green, Color.Blue),
+                        center = Offset(Float.POSITIVE_INFINITY, 0f)
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = SweepGradientShader(
+                        Offset(100f, 0f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
+    @Test
+    fun testSweepGradientWithStops() {
+        testDrawScopeAndCanvasAreEquivalent(
+            100,
+            100,
+            {
+                drawRect(
+                    Brush.sweepGradient(
+                        0.0f to Color.Red,
+                        0.1f to Color.Green,
+                        0.8f to Color.Blue
+                    )
+                )
+            },
+            { canvas ->
+                val paint = Paint().apply {
+                    shader = SweepGradientShader(
+                        Offset(50f, 50f),
+                        colors = listOf(Color.Red, Color.Green, Color.Blue),
+                        colorStops = listOf(0.0f, 0.1f, 0.8f)
+                    )
+                }
+                canvas.drawRect(0f, 0f, 100f, 100f, paint)
+            }
+        )
+    }
+
     private inline fun testDrawTransformDefault(block: WrappedDrawTransform.() -> Unit) {
         val width = 100
         val height = 150
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt
index 2eca0e3..30b33a1 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt
@@ -108,7 +108,17 @@
             internalPaint.setNativeColorFilter(value)
         }
 
-    override var nativePathEffect: NativePathEffect? = null
+    override var nativePathEffect: NativePathEffect?
+        get() = pathEffect?.asAndroidPathEffect()
+        set(value) {
+            pathEffect = if (value == null) {
+                null
+            } else {
+                AndroidPathEffect(value)
+            }
+        }
+
+    override var pathEffect: PathEffect? = null
         set(value) {
             internalPaint.setNativePathEffect(value)
             field = value
@@ -235,6 +245,6 @@
     this.shader = value
 }
 
-internal fun NativePaint.setNativePathEffect(value: NativePathEffect?) {
-    this.pathEffect = value
+internal fun NativePaint.setNativePathEffect(value: PathEffect?) {
+    this.pathEffect = (value as AndroidPathEffect).nativePathEffect
 }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.kt
new file mode 100644
index 0000000..4755271
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import android.graphics.PathDashPathEffect
+
+/**
+ * Obtain a reference to the Android PathEffect type
+ */
+internal class AndroidPathEffect(val nativePathEffect: android.graphics.PathEffect) : PathEffect
+
+fun PathEffect.asAndroidPathEffect(): android.graphics.PathEffect =
+    (this as AndroidPathEffect).nativePathEffect
+
+fun android.graphics.PathEffect.toComposePathEffect(): PathEffect = AndroidPathEffect(this)
+
+internal actual fun actualCornerPathEffect(radius: Float): PathEffect =
+    AndroidPathEffect(android.graphics.CornerPathEffect(radius))
+
+internal actual fun actualDashPathEffect(intervals: FloatArray, phase: Float): PathEffect =
+    AndroidPathEffect(android.graphics.DashPathEffect(intervals, phase))
+
+internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
+    AndroidPathEffect(
+        android.graphics.ComposePathEffect(
+            (outer as AndroidPathEffect).nativePathEffect,
+            (inner as AndroidPathEffect).nativePathEffect
+        )
+    )
+
+internal actual fun actualStampedPathEffect(
+    shape: Path,
+    advance: Float,
+    phase: Float,
+    style: StampedPathEffectStyle
+): PathEffect =
+    AndroidPathEffect(
+        PathDashPathEffect(
+            shape.asAndroidPath(),
+            advance,
+            phase,
+            style.toAndroidPathDashPathEffectStyle()
+        )
+    )
+
+internal fun StampedPathEffectStyle.toAndroidPathDashPathEffectStyle() =
+    when (this) {
+        StampedPathEffectStyle.Morph -> PathDashPathEffect.Style.MORPH
+        StampedPathEffectStyle.Rotate -> PathDashPathEffect.Style.ROTATE
+        StampedPathEffectStyle.Translate -> PathDashPathEffect.Style.TRANSLATE
+    }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
index dfb9be7..5f01051 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
@@ -20,15 +20,387 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.util.nativeClass
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.unit.center
 
 @Immutable
 sealed class Brush {
-    abstract fun applyTo(p: Paint, alpha: Float)
+    @Deprecated(
+        "Use applyTo(size, paint, alpha) instead",
+        ReplaceWith("applyTo(size, p, alpha", "androidx.compose.ui.graphics")
+    )
+    open fun applyTo(p: Paint, alpha: Float) = applyTo(Size.Zero, p, alpha)
+
+    abstract fun applyTo(size: Size, p: Paint, alpha: Float)
+
+    companion object {
+
+        /**
+         * Creates a linear gradient with the provided colors along the given start and end
+         * coordinates. The colors are dispersed at the provided offset defined in the [ColorStop]
+         *
+         * ```
+         *  Brush.linearGradient(
+         *      0.0f to Color.Red,
+         *      0.3f to Color.Green,
+         *      1.0f to Color.Blue,
+         *      start = Offset(0.0f, 50.0f),
+         *      end = Offset(0.0f, 100.0f)
+         * )
+         * ```
+         *
+         * @see androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colorStops Colors and their offset in the gradient area
+         * @param start Starting position of the linear gradient. This can be set to
+         * [Offset.Infinite] to position at the far right and bottom of the drawing area
+         * @param end Ending position of the linear gradient. This can be set to
+         * [Offset.Infinite] to position at the far right and bottom of the drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun linearGradient(
+            vararg colorStops: ColorStop,
+            start: Offset = Offset.Zero,
+            end: Offset = Offset.Infinite,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = LinearGradient(
+            colors = List<Color>(colorStops.size) { i -> colorStops[i].second },
+            stops = List<Float>(colorStops.size) { i -> colorStops[i].first },
+            start = start,
+            end = end,
+            tileMode = tileMode
+        )
+
+        /**
+         * Creates a linear gradient with the provided colors along the given start and end coordinates.
+         * The colors are
+         *
+         * ```
+         *  Brush.linearGradient(
+         *      listOf(Color.Red, Color.Green, Color.Blue),
+         *      start = Offset(0.0f, 50.0f)
+         *      end = Offset(0.0f, 100.0f)
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colors Colors to be rendered as part of the gradient
+         * @param start Starting position of the linear gradient. This can be set to
+         * [Offset.Infinite] to position at the far right and bottom of the drawing area
+         * @param end Ending position of the linear gradient. This can be set to
+         * [Offset.Infinite] to position at the far right and bottom of the drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun linearGradient(
+            colors: List<Color>,
+            start: Offset = Offset.Zero,
+            end: Offset = Offset.Infinite,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = LinearGradient(
+            colors = colors,
+            stops = null,
+            start = start,
+            end = end,
+            tileMode = tileMode
+        )
+
+        /**
+         * Creates a horizontal gradient with the given colors evenly dispersed within the gradient
+         *
+         * Ex:
+         * ```
+         *  Brush.horizontalGradient(
+         *      listOf(Color.Red, Color.Green, Color.Blue),
+         *      startX = 10.0f,
+         *      endX = 20.0f
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colors colors Colors to be rendered as part of the gradient
+         * @param startX Starting x position of the horizontal gradient. Defaults to 0 which
+         * represents the left of the drawing area
+         * @param endX Ending x position of the horizontal gradient.
+         * Defaults to [Float.POSITIVE_INFINITY] which indicates the right of the specified
+         * drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun horizontalGradient(
+            colors: List<Color>,
+            startX: Float = 0.0f,
+            endX: Float = Float.POSITIVE_INFINITY,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = linearGradient(colors, Offset(startX, 0.0f), Offset(endX, 0.0f), tileMode)
+
+        /**
+         * Creates a horizontal gradient with the given colors dispersed at the provided offset
+         * defined in the [ColorStop]
+         *
+         * Ex:
+         * ```
+         *  Brush.horizontalGradient(
+         *      0.0f to Color.Red,
+         *      0.3f to Color.Green,
+         *      1.0f to Color.Blue,
+         *      startX = 0.0f,
+         *      endX = 100.0f
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
+         * the vertical gradient
+         * @param startX Starting x position of the horizontal gradient. Defaults to 0 which
+         * represents the left of the drawing area
+         * @param endX Ending x position of the horizontal gradient.
+         * Defaults to [Float.POSITIVE_INFINITY] which indicates the right of the specified
+         * drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun horizontalGradient(
+            vararg colorStops: ColorStop,
+            startX: Float = 0.0f,
+            endX: Float = Float.POSITIVE_INFINITY,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = linearGradient(
+            *colorStops,
+            start = Offset(startX, 0.0f),
+            end = Offset(endX, 0.0f),
+            tileMode = tileMode
+        )
+
+        /**
+         * Creates a vertical gradient with the given colors evenly dispersed within the gradient
+         * Ex:
+         * ```
+         *  Brush.verticalGradient(
+         *      listOf(Color.Red, Color.Green, Color.Blue),
+         *      startY = 0.0f,
+         *      endY = 100.0f
+         * )
+         *
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colors colors Colors to be rendered as part of the gradient
+         * @param startY Starting y position of the vertical gradient. Defaults to 0 which
+         * represents the top of the drawing area
+         * @param endY Ending y position of the vertical gradient.
+         * Defaults to [Float.POSITIVE_INFINITY] which indicates the bottom of the specified
+         * drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun verticalGradient(
+            colors: List<Color>,
+            startY: Float = 0.0f,
+            endY: Float = Float.POSITIVE_INFINITY,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = linearGradient(colors, Offset(0.0f, startY), Offset(0.0f, endY), tileMode)
+
+        /**
+         * Creates a vertical gradient with the given colors at the provided offset defined
+         * in the [ColorStop]
+         *
+         * Ex:
+         * ```
+         *  Brush.verticalGradient(
+         *      0.1f to Color.Red,
+         *      0.3f to Color.Green,
+         *      0.5f to Color.Blue,
+         *      startY = 0.0f,
+         *      endY = 100.0f
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
+         * the vertical gradient
+         * @param startY Starting y position of the vertical gradient. Defaults to 0 which
+         * represents the top of the drawing area
+         * @param endY Ending y position of the vertical gradient.
+         * Defaults to [Float.POSITIVE_INFINITY] which indicates the bottom of the specified
+         * drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun verticalGradient(
+            vararg colorStops: ColorStop,
+            startY: Float = 0f,
+            endY: Float = Float.POSITIVE_INFINITY,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = linearGradient(
+            *colorStops,
+            start = Offset(0.0f, startY),
+            end = Offset(0.0f, endY),
+            tileMode = tileMode
+        )
+
+        /**
+         * Creates a radial gradient with the given colors at the provided offset
+         * defined in the [ColorStop]
+         * ```
+         * Brush.radialGradient(
+         *      0.0f to Color.Red,
+         *      0.3f to Color.Green,
+         *      1.0f to Color.Blue,
+         *      center = Offset(side1 / 2.0f, side2 / 2.0f),
+         *      radius = side1 / 2.0f,
+         *      tileMode = TileMode.Repeated
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
+         * the radial gradient
+         * @param center Center position of the radial gradient circle. If this is set to
+         * [Offset.Unspecified] then the center of the drawing area is used as the center for
+         * the radial gradient. [Float.POSITIVE_INFINITY] can be used for either [Offset.x] or
+         * [Offset.y] to indicate the far right or far bottom of the drawing area respectively.
+         * @param radius Radius for the radial gradient. Defaults to positive infinity to indicate
+         * the largest radius that can fit within the bounds of the drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun radialGradient(
+            vararg colorStops: ColorStop,
+            center: Offset = Offset.Unspecified,
+            radius: Float = Float.POSITIVE_INFINITY,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = RadialGradient(
+            colors = List<Color>(colorStops.size) { i -> colorStops[i].second },
+            stops = List<Float>(colorStops.size) { i -> colorStops[i].first },
+            center = center,
+            radius = radius,
+            tileMode = tileMode
+        )
+
+        /**
+         * Creates a radial gradient with the given colors evenly dispersed within the gradient
+         * ```
+         * Brush.radialGradient(
+         *      listOf(Color.Red, Color.Green, Color.Blue),
+         *      centerX = side1 / 2.0f,
+         *      centerY = side2 / 2.0f,
+         *      radius = side1 / 2.0f,
+         *      tileMode = TileMode.Repeated
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colors Colors to be rendered as part of the gradient
+         * @param center Center position of the radial gradient circle. If this is set to
+         * [Offset.Unspecified] then the center of the drawing area is used as the center for
+         * the radial gradient. [Float.POSITIVE_INFINITY] can be used for either [Offset.x] or
+         * [Offset.y] to indicate the far right or far bottom of the drawing area respectively.
+         * @param radius Radius for the radial gradient. Defaults to positive infinity to indicate
+         * the largest radius that can fit within the bounds of the drawing area
+         * @param tileMode Determines the behavior for how the shader is to fill a region outside
+         * its bounds. Defaults to [TileMode.Clamp] to repeat the edge pixels
+         */
+        @Stable
+        fun radialGradient(
+            colors: List<Color>,
+            center: Offset = Offset.Unspecified,
+            radius: Float = Float.POSITIVE_INFINITY,
+            tileMode: TileMode = TileMode.Clamp
+        ): Brush = RadialGradient(
+            colors = colors,
+            stops = null,
+            center = center,
+            radius = radius,
+            tileMode = tileMode
+        )
+
+        /**
+         * Creates a sweep gradient with the given colors dispersed around the center with
+         * offsets defined in each [ColorStop]. The sweep begins relative to 3 o'clock and continues
+         * clockwise until it reaches the starting position again.
+         *
+         * Ex:
+         * ```
+         *  Brush.sweepGradient(
+         *      0.0f to Color.Red,
+         *      0.3f to Color.Green,
+         *      1.0f to Color.Blue,
+         *      center = Offset(0.0f, 100.0f)
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
+         * the sweep gradient
+         * @param center Center position of the sweep gradient circle. If this is set to
+         * [Offset.Unspecified] then the center of the drawing area is used as the center for
+         * the sweep gradient
+         */
+        @Stable
+        fun sweepGradient(
+            vararg colorStops: ColorStop,
+            center: Offset = Offset.Unspecified
+        ): Brush = SweepGradient(
+            colors = List<Color>(colorStops.size) { i -> colorStops[i].second },
+            stops = List<Float>(colorStops.size) { i -> colorStops[i].first },
+            center = center
+        )
+
+        /**
+         * Creates a sweep gradient with the given colors dispersed evenly around the center.
+         * The sweep begins relative to 3 o'clock and continues clockwise until it reaches the
+         * starting position again.
+         *
+         * Ex:
+         * ```
+         *  Brush.sweepGradient(
+         *      listOf(Color.Red, Color.Green, Color.Blue),
+         *      center = Offset(10.0f, 20.0f)
+         * )
+         * ```
+         *
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
+         *
+         * @param colors List of colors to fill the sweep gradient
+         * @param center Center position of the sweep gradient circle. If this is set to
+         * [Offset.Unspecified] then the center of the drawing area is used as the center for
+         * the sweep gradient
+         */
+        @Stable
+        fun sweepGradient(
+            colors: List<Color>,
+            center: Offset = Offset.Unspecified
+        ): Brush = SweepGradient(
+            colors = colors,
+            stops = null,
+            center = center
+        )
+    }
 }
 
 @Immutable
 class SolidColor(val value: Color) : Brush() {
-    override fun applyTo(p: Paint, alpha: Float) {
+    override fun applyTo(size: Size, p: Paint, alpha: Float) {
         p.alpha = DefaultAlpha
         p.color = if (alpha != DefaultAlpha) {
             value.copy(alpha = value.alpha * alpha)
@@ -74,6 +446,16 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.linearGradient instead",
+    ReplaceWith(
+        "Brush.linearGradient(colors = colors," +
+            "start = Offset(startX, startY), " +
+            "end = Offset(endX, endY), " +
+            "tileMode = tileMode)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun LinearGradient(
     colors: List<Color>,
@@ -85,10 +467,8 @@
 ) = LinearGradient(
     colors,
     null,
-    startX,
-    startY,
-    endX,
-    endY,
+    Offset(startX, startY),
+    Offset(endX, endY),
     tileMode
 )
 
@@ -108,6 +488,16 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.linearGradient instead",
+    ReplaceWith(
+        "Brush.linearGradient(colorStops, " +
+            "start = Offset(startX, startY), " +
+            "end = Offset(endX, endY), " +
+            "tileMode = tileMode)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun LinearGradient(
     vararg colorStops: ColorStop,
@@ -119,10 +509,8 @@
 ) = LinearGradient(
     List<Color>(colorStops.size) { i -> colorStops[i].second },
     List<Float>(colorStops.size) { i -> colorStops[i].first },
-    startX,
-    startY,
-    endX,
-    endY,
+    Offset(startX, startY),
+    Offset(endX, endY),
     tileMode
 )
 
@@ -140,6 +528,18 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.radialGradient instead",
+    ReplaceWith(
+        "Brush.radialGradient(" +
+            "colorStops, " +
+            "center = Offset(centerX, centerY)," +
+            "radius = radius, " +
+            "tileMode = tileMode" +
+            ")",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun RadialGradient(
     vararg colorStops: ColorStop,
@@ -150,8 +550,7 @@
 ) = RadialGradient(
     List<Color>(colorStops.size) { i -> colorStops[i].second },
     List<Float>(colorStops.size) { i -> colorStops[i].first },
-    centerX,
-    centerY,
+    Offset(centerX, centerY),
     radius,
     tileMode
 )
@@ -168,6 +567,13 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.radialGradient instead",
+    ReplaceWith(
+        "Brush.radialGradient(colors, Offset(centerX, centerY), radius, tileMode)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun RadialGradient(
     colors: List<Color>,
@@ -175,7 +581,7 @@
     centerY: Float,
     radius: Float,
     tileMode: TileMode = TileMode.Clamp
-) = RadialGradient(colors, null, centerX, centerY, radius, tileMode)
+) = RadialGradient(colors, null, Offset(centerX, centerY), radius, tileMode)
 
 /**
  * Creates a vertical gradient with the given colors evenly dispersed within the gradient
@@ -189,6 +595,13 @@
  *
  * ```
  */
+@Deprecated(
+    "Use Brush.verticalGradient instead",
+    ReplaceWith(
+        "Brush.verticalGradient(colors, startY, endY, tileMode)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun VerticalGradient(
     colors: List<Color>,
@@ -198,10 +611,8 @@
 ) = LinearGradient(
     colors,
     null,
-    startX = 0.0f,
-    startY = startY,
-    endX = 0.0f,
-    endY = endY,
+    start = Offset(0.0f, startY),
+    end = Offset(0.0f, endY),
     tileMode = tileMode
 )
 
@@ -218,6 +629,13 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.verticalGradient instead",
+    ReplaceWith(
+        "Brush.verticalGradient(colorStops, startY = startY, endY = endY, tileMode = tileMode)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun VerticalGradient(
     vararg colorStops: ColorStop,
@@ -227,10 +645,8 @@
 ) = LinearGradient(
     List<Color>(colorStops.size) { i -> colorStops[i].second },
     List<Float>(colorStops.size) { i -> colorStops[i].first },
-    startX = 0.0f,
-    startY = startY,
-    endX = 0.0f,
-    endY = endY,
+    start = Offset(0.0f, startY),
+    end = Offset(0.0f, endY),
     tileMode = tileMode
 )
 
@@ -246,6 +662,13 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.horizontalGradient instead",
+    ReplaceWith(
+        "Brush.horizontalGradient(colors, startX, endX, tileMode)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun HorizontalGradient(
     colors: List<Color>,
@@ -255,10 +678,8 @@
 ) = LinearGradient(
     colors,
     null,
-    startX = startX,
-    startY = 0.0f,
-    endX = endX,
-    endY = 0.0f,
+    start = Offset(startX, 0.0f),
+    end = Offset(endX, 0.0f),
     tileMode = tileMode
 )
 
@@ -276,6 +697,14 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.horizontalGradient instead",
+    ReplaceWith(
+        "Brush.horizontalGradient(" +
+            "colorStops, startX = startX, endX = endX, tileMode = tileMode)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun HorizontalGradient(
     vararg colorStops: ColorStop,
@@ -285,10 +714,8 @@
 ) = LinearGradient(
     List<Color>(colorStops.size) { i -> colorStops[i].second },
     List<Float>(colorStops.size) { i -> colorStops[i].first },
-    startX = startX,
-    startY = 0.0f,
-    endX = endX,
-    endY = 0.0f,
+    start = Offset(startX, 0.0f),
+    end = Offset(endX, 0.0f),
     tileMode = tileMode
 )
 
@@ -307,6 +734,13 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.sweepGradient instead",
+    ReplaceWith(
+        "Brush.sweepGradient(colorStops, center = center)",
+        "androidx.compose.ui.ui.graphics"
+    )
+)
 @Stable
 fun SweepGradient(
     vararg colorStops: ColorStop,
@@ -314,7 +748,7 @@
 ) = SweepGradient(
     center,
     List<Color>(colorStops.size) { i -> colorStops[i].second },
-    List<Float>(colorStops.size) { i -> colorStops[i].first },
+    List<Float>(colorStops.size) { i -> colorStops[i].first }
 )
 
 /**
@@ -330,6 +764,10 @@
  * )
  * ```
  */
+@Deprecated(
+    "Use Brush.sweepGradient instead",
+    ReplaceWith("Brush.sweepGradient(colors, center)", "androidx.compose.ui.ui.graphics")
+)
 @Stable
 fun SweepGradient(
     colors: List<Color>,
@@ -343,20 +781,25 @@
 class LinearGradient internal constructor(
     private val colors: List<Color>,
     private val stops: List<Float>? = null,
-    private val startX: Float,
-    private val startY: Float,
-    private val endX: Float,
-    private val endY: Float,
+    private val start: Offset,
+    private val end: Offset,
     private val tileMode: TileMode = TileMode.Clamp
-) : ShaderBrush(
-    LinearGradientShader(
-        Offset(startX, startY),
-        Offset(endX, endY),
-        colors,
-        stops,
-        tileMode
-    )
-) {
+) : ShaderBrush() {
+
+    override fun createShader(size: Size): Shader {
+        val startX = if (start.x == Float.POSITIVE_INFINITY) size.width else start.x
+        val startY = if (start.y == Float.POSITIVE_INFINITY) size.height else start.y
+        val endX = if (end.x == Float.POSITIVE_INFINITY) size.width else end.x
+        val endY = if (end.y == Float.POSITIVE_INFINITY) size.height else end.y
+        return LinearGradientShader(
+            colors = colors,
+            colorStops = stops,
+            from = Offset(startX, startY),
+            to = Offset(endX, endY),
+            tileMode = tileMode
+        )
+    }
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (nativeClass() != other?.nativeClass()) return false
@@ -365,10 +808,8 @@
 
         if (colors != other.colors) return false
         if (stops != other.stops) return false
-        if (startX != other.startX) return false
-        if (startY != other.startY) return false
-        if (endX != other.endX) return false
-        if (endY != other.endY) return false
+        if (start != other.start) return false
+        if (end != other.end) return false
         if (tileMode != other.tileMode) return false
 
         return true
@@ -377,21 +818,19 @@
     override fun hashCode(): Int {
         var result = colors.hashCode()
         result = 31 * result + (stops?.hashCode() ?: 0)
-        result = 31 * result + startX.hashCode()
-        result = 31 * result + startY.hashCode()
-        result = 31 * result + endX.hashCode()
-        result = 31 * result + endY.hashCode()
+        result = 31 * result + start.hashCode()
+        result = 31 * result + end.hashCode()
         result = 31 * result + tileMode.hashCode()
         return result
     }
 
     override fun toString(): String {
+        val startValue = if (start.isFinite) "start=$start, " else ""
+        val endValue = if (end.isFinite) "end=$end, " else ""
         return "LinearGradient(colors=$colors, " +
             "stops=$stops, " +
-            "startX=$startX, " +
-            "startY=$startY, " +
-            "endX=$endX, " +
-            "endY=$endY, " +
+            startValue +
+            endValue +
             "tileMode=$tileMode)"
     }
 }
@@ -403,19 +842,32 @@
 class RadialGradient internal constructor(
     private val colors: List<Color>,
     private val stops: List<Float>? = null,
-    private val centerX: Float,
-    private val centerY: Float,
+    private val center: Offset,
     private val radius: Float,
     private val tileMode: TileMode = TileMode.Clamp
-) : ShaderBrush(
-    RadialGradientShader(
-        Offset(centerX, centerY),
-        radius,
-        colors,
-        stops,
-        tileMode
-    )
-) {
+) : ShaderBrush() {
+
+    override fun createShader(size: Size): Shader {
+        val centerX: Float
+        val centerY: Float
+        if (center.isUnspecified) {
+            val drawCenter = size.center()
+            centerX = drawCenter.x
+            centerY = drawCenter.y
+        } else {
+            centerX = if (center.x == Float.POSITIVE_INFINITY) size.width else center.x
+            centerY = if (center.y == Float.POSITIVE_INFINITY) size.height else center.y
+        }
+
+        return RadialGradientShader(
+            colors = colors,
+            colorStops = stops,
+            center = Offset(centerX, centerY),
+            radius = if (radius == Float.POSITIVE_INFINITY) size.minDimension / 2 else radius,
+            tileMode = tileMode
+        )
+    }
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (nativeClass() != other?.nativeClass()) return false
@@ -424,8 +876,7 @@
 
         if (colors != other.colors) return false
         if (stops != other.stops) return false
-        if (centerX != other.centerX) return false
-        if (centerY != other.centerY) return false
+        if (center != other.center) return false
         if (radius != other.radius) return false
         if (tileMode != other.tileMode) return false
 
@@ -435,20 +886,20 @@
     override fun hashCode(): Int {
         var result = colors.hashCode()
         result = 31 * result + (stops?.hashCode() ?: 0)
-        result = 31 * result + centerX.hashCode()
-        result = 31 * result + centerY.hashCode()
+        result = 31 * result + center.hashCode()
         result = 31 * result + radius.hashCode()
         result = 31 * result + tileMode.hashCode()
         return result
     }
 
     override fun toString(): String {
+        val centerValue = if (center.isSpecified) "center=$center, " else ""
+        val radiusValue = if (radius.isFinite()) "radius=$radius, " else ""
         return "RadialGradient(" +
             "colors=$colors, " +
             "stops=$stops, " +
-            "centerX=$centerX, " +
-            "centerY=$centerY, " +
-            "radius=$radius, " +
+            centerValue +
+            radiusValue +
             "tileMode=$tileMode)"
     }
 }
@@ -460,14 +911,23 @@
 class SweepGradient internal constructor(
     private val center: Offset,
     private val colors: List<Color>,
-    private val stops: List<Float>? = null,
-) : ShaderBrush(
-    SweepGradientShader(
-        center,
-        colors,
-        stops
-    )
-) {
+    private val stops: List<Float>? = null
+) : ShaderBrush() {
+
+    override fun createShader(size: Size): Shader =
+        SweepGradientShader(
+            if (center.isUnspecified) {
+                size.center()
+            } else {
+                Offset(
+                    if (center.x == Float.POSITIVE_INFINITY) size.width else center.x,
+                    if (center.y == Float.POSITIVE_INFINITY) size.height else center.y
+                )
+            },
+            colors,
+            stops
+        )
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (nativeClass() != other?.nativeClass()) return false
@@ -489,18 +949,45 @@
     }
 
     override fun toString(): String {
-        return "SweepGradient(center=$center, colors=$colors, stops=$stops)"
+        val centerValue = if (center.isSpecified) "center=$center, " else ""
+        return "SweepGradient(" +
+            centerValue +
+            "colors=$colors, stops=$stops)"
     }
 }
 
 /**
+ * Convenience method to create a ShaderBrush that always returns the same shader instance
+ * regardless of size
+ */
+fun ShaderBrush(shader: Shader) = object : ShaderBrush() {
+
+    /**
+     * Create a shader based on the given size that represents the current drawing area
+     */
+    override fun createShader(size: Size): Shader = shader
+}
+
+/**
  * Brush implementation that wraps and applies a the provided shader to a [Paint]
+ * The shader can be lazily created based on a given size, or provided directly as a parameter
  */
 @Immutable
-open class ShaderBrush(val shader: Shader) : Brush() {
-    final override fun applyTo(p: Paint, alpha: Float) {
+abstract class ShaderBrush() : Brush() {
+
+    private var internalShader: Shader? = null
+    private var createdSize = Size.Unspecified
+
+    abstract fun createShader(size: Size): Shader
+
+    final override fun applyTo(size: Size, p: Paint, alpha: Float) {
+        var shader = internalShader
+        if (shader == null || createdSize != size) {
+            shader = createShader(size).also { internalShader = it }
+            createdSize = size
+        }
         if (p.color != Color.Black) p.color = Color.Black
         if (p.shader != shader) p.shader = shader
         if (p.alpha != alpha) p.alpha = alpha
     }
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt
index bdbd687..32a3905 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt
@@ -28,111 +28,126 @@
 interface Paint {
     fun asFrameworkPaint(): NativePaint
 
+    /**
+     * Configures the alpha value between 0f to 1f representing fully transparent to fully
+     * opaque for the color drawn with this Paint
+     */
     var alpha: Float
 
-    // Whether to apply anti-aliasing to lines and images drawn on the
-    // canvas.
-    //
-    // Defaults to true.
+    /**
+     * Whether to apply anti-aliasing to lines and images drawn on the
+     * canvas.
+     * Defaults to true.
+     */
     var isAntiAlias: Boolean
 
-    // The color to use when stroking or filling a shape.
-    //
-    // Defaults to opaque black.
-    //
-    // See also:
-    //
-    //  * [style], which controls whether to stroke or fill (or both).
-    //  * [colorFilter], which overrides [color].
-    //  * [shader], which overrides [color] with more elaborate effects.
-    //
-    // This color is not used when compositing. To colorize a layer, use
-    // [colorFilter].
+    /**
+     * The color to use when stroking or filling a shape.
+     * Defaults to opaque black.
+     * See also:
+     * [style], which controls whether to stroke or fill (or both).
+     * [colorFilter], which overrides [color].
+     * [shader], which overrides [color] with more elaborate effects.
+     * This color is not used when compositing. To colorize a layer, use [colorFilter].
+     */
     var color: Color
 
-    // A blend mode to apply when a shape is drawn or a layer is composited.
-    //
-    // The source colors are from the shape being drawn (e.g. from
-    // [Canvas.drawPath]) or layer being composited (the graphics that were drawn
-    // between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying
-    // the [colorFilter], if any.
-    //
-    // The destination colors are from the background onto which the shape or
-    // layer is being composited.
-    //
-    // Defaults to [BlendMode.srcOver].
-    //
-    // See also:
-    //
-    //  * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite
-    //    the layer when [restore] is called.
-    //  * [BlendMode], which discusses the user of [saveLayer] with [blendMode].
+    /**
+     * A blend mode to apply when a shape is drawn or a layer is composited.
+     * The source colors are from the shape being drawn (e.g. from
+     * [Canvas.drawPath]) or layer being composited (the graphics that were drawn
+     * between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying
+     * the [colorFilter], if any.
+     * The destination colors are from the background onto which the shape or
+     * layer is being composited.
+     * Defaults to [BlendMode.SrcOver].
+     * See also:
+     * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite
+     * the layer when [Canvas.restore] is called.
+     * [BlendMode], which discusses the user of [Canvas.saveLayer] with [blendMode].
+     */
     var blendMode: BlendMode
 
-    // Whether to paint inside shapes, the edges of shapes, or both.
-    //
-    // Defaults to [PaintingStyle.fill].
+    /**
+     * Whether to paint inside shapes, the edges of shapes, or both.
+     * Defaults to [PaintingStyle.Fill].
+     */
     var style: PaintingStyle
 
-    // How wide to make edges drawn when [style] is set to
-    // [PaintingStyle.stroke]. The width is given in logical pixels measured in
-    // the direction orthogonal to the direction of the path.
-    //
-    // Defaults to 0.0, which correspond to a hairline width.
+    /**
+     * How wide to make edges drawn when [style] is set to
+     * [PaintingStyle.Stroke]. The width is given in logical pixels measured in
+     * the direction orthogonal to the direction of the path.
+     * Defaults to 0.0, which correspond to a hairline width.
+     */
     var strokeWidth: Float
 
-    // The kind of finish to place on the end of lines drawn when
-    // [style] is set to [PaintingStyle.stroke].
-    //
-    // Defaults to [StrokeCap.butt], i.e. no caps.
+    /**
+     * The kind of finish to place on the end of lines drawn when
+     * [style] is set to [PaintingStyle.Stroke].
+     * Defaults to [StrokeCap.Butt], i.e. no caps.
+     */
     var strokeCap: StrokeCap
 
-    // The kind of finish to place on the joins between segments.
-    //
-    // This applies to paths drawn when [style] is set to [PaintingStyle.stroke],
-    // It does not apply to points drawn as lines with [Canvas.drawPoints].
-    //
-    // Defaults to [StrokeJoin.miter], i.e. sharp corners. See also
-    // [strokeMiterLimit] to control when miters are replaced by bevels.
+    /**
+     * The kind of finish to place on the joins between segments.
+     * This applies to paths drawn when [style] is set to [PaintingStyle.Stroke],
+     * It does not apply to points drawn as lines with [Canvas.drawPoints].
+     * Defaults to [StrokeJoin.Miter], i.e. sharp corners. See also
+     * [strokeMiterLimit] to control when miters are replaced by bevels.
+     */
     var strokeJoin: StrokeJoin
 
-    // The limit for miters to be drawn on segments when the join is set to
-    // [StrokeJoin.miter] and the [style] is set to [PaintingStyle.stroke]. If
-    // this limit is exceeded, then a [StrokeJoin.bevel] join will be drawn
-    // instead. This may cause some 'popping' of the corners of a path if the
-    // angle between line segments is animated.
-    //
-    // This limit is expressed as a limit on the length of the miter.
-    //
-    // Defaults to 4.0.  Using zero as a limit will cause a [StrokeJoin.bevel]
-    // join to be used all the time.
+    /**
+     * The limit for miters to be drawn on segments when the join is set to
+     * [StrokeJoin.Miter] and the [style] is set to [PaintingStyle.Stroke]. If
+     * this limit is exceeded, then a [StrokeJoin.Bevel] join will be drawn
+     * instead. This may cause some 'popping' of the corners of a path if the
+     * angle between line segments is animated.
+     * This limit is expressed as a limit on the length of the miter.
+     * Defaults to 4.0.  Using zero as a limit will cause a [StrokeJoin.Bevel]
+     * join to be used all the time.
+     */
     var strokeMiterLimit: Float
 
-    // Controls the performance vs quality trade-off to use when applying
-    // when drawing images, as with [Canvas.drawImageRect]
-    //
-    // Defaults to [FilterQuality.none].
+    /**
+     * Controls the performance vs quality trade-off to use when applying
+     * when drawing images, as with [Canvas.drawImageRect]
+     * Defaults to [FilterQuality.None].
+     */
     var filterQuality: FilterQuality
 
-    // The shader to use when stroking or filling a shape.
-    //
-    // When this is null, the [color] is used instead.
-    //
-    // See also:
-    //
-    //  * [Gradient], a shader that paints a color gradient.
-    //  * [ImageShader], a shader that tiles an [Image].
-    //  * [colorFilter], which overrides [shader].
-    //  * [color], which is used if [shader] and [colorFilter] are null.
+    /**
+     * The shader to use when stroking or filling a shape.
+     *
+     * When this is null, the [color] is used instead.
+     *
+     * See also:
+     * [LinearGradientShader], [RadialGradientShader], or [SweepGradientShader] shaders that
+     * paint a color gradient.
+     * [ImageShader], a shader that tiles an [ImageBitmap].
+     * [colorFilter], which overrides [shader].
+     * [color], which is used if [shader] and [colorFilter] are null.
+     */
     var shader: Shader?
 
-    // A color filter to apply when a shape is drawn or when a layer is
-    // composited.
-    //
-    // See [ColorFilter] for details.
-    //
-    // When a shape is being drawn, [colorFilter] overrides [color] and [shader].
+    /**
+     *  A color filter to apply when a shape is drawn or when a layer is
+     *  composited.
+     *  See [ColorFilter] for details.
+     *  When a shape is being drawn, [colorFilter] overrides [color] and [shader].
+     */
     var colorFilter: ColorFilter?
 
+    @Suppress("DEPRECATION")
+    @Deprecated(
+        "Use pathEffect instead",
+        ReplaceWith("pathEffect", "androidx.compose.ui.graphics.Paint")
+    )
     var nativePathEffect: NativePathEffect?
+
+    /**
+     * Specifies the [PathEffect] applied to the geometry of the shape that is drawn
+     */
+    var pathEffect: PathEffect?
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
index 91e53db..ccf90c0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
@@ -22,6 +22,10 @@
 
 expect fun Path(): Path
 
+@Deprecated(
+    "Use PathEffect instead",
+    ReplaceWith("PathEffect", "androidx.compose.ui.graphics.PathEffect")
+)
 expect class NativePathEffect
 
 /* expect class */ interface Path {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathEffect.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathEffect.kt
new file mode 100644
index 0000000..fb12a5d
--- /dev/null
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathEffect.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+/**
+ * Effect applied to the geometry of a drawing primitive. For example, this can be used
+ * to draw shapes as a dashed or shaped pattern, or apply a treatment around line segment
+ * intersections.
+ */
+interface PathEffect {
+    companion object {
+
+        /**
+         * Replaces sharp angles between line segments into rounded angles of the specified radius
+         *
+         * @param radius Rounded corner radius to apply for each angle of the drawn shape
+         */
+        fun cornerPathEffect(radius: Float): PathEffect = actualCornerPathEffect(radius)
+
+        /**
+         * Draws a shape as a series of dashes with the given intervals and offset into the specified
+         * interval array. The intervals must contain an even number of entries (>=2). The even indices
+         * specify "on" intervals and the odd indices represent "off" intervals. The phase parameter
+         * is the pixel offset into the intervals array (mod the sum of all of the intervals).
+         *
+         * For example: if `intervals[] = {10, 20}`, and phase = 25, this will set up a dashed
+         * path like so: 5 pixels off 10 pixels on 20 pixels off 10 pixels on 20 pixels off
+         *
+         * The phase parameter is
+         * an offset into the intervals array. The intervals array
+         * controls the length of the dashes. This is only applied for stroked shapes
+         * (ex. [PaintingStyle.Stroke] and is ignored for filled in shapes (ex. [PaintingStyle.Fill]
+         *
+         * @param intervals Array of "on" and "off" distances for the dashed line segments
+         * @param phase Pixel offset into the intervals array
+         */
+        fun dashPathEffect(intervals: FloatArray, phase: Float = 0f): PathEffect =
+            actualDashPathEffect(intervals, phase)
+
+        /**
+         * Create a PathEffect that applies the inner effect to the path, and then applies the outer
+         * effect to the result of the inner effect. (e.g. outer(inner(path)).
+         */
+        fun chainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
+            actualChainPathEffect(outer, inner)
+
+        /**
+         * Dash the drawn path by stamping it with the specified shape represented as a [Path].
+         * This is only applied to stroke shapes and will be ignored with filled shapes.
+         * The stroke width used with this [PathEffect] is ignored as well.
+         *
+         * @param shape Path to stamp along
+         * @param advance Spacing between each stamped shape
+         * @param phase Amount to offset before the first shape is stamped
+         * @param style How to transform the shape at each position as it is stamped
+         */
+        fun stampedPathEffect(
+            shape: Path,
+            advance: Float,
+            phase: Float,
+            style: StampedPathEffectStyle
+        ): PathEffect = actualStampedPathEffect(shape, advance, phase, style)
+    }
+}
+
+internal expect fun actualCornerPathEffect(radius: Float): PathEffect
+
+internal expect fun actualDashPathEffect(intervals: FloatArray, phase: Float): PathEffect
+
+internal expect fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect
+
+internal expect fun actualStampedPathEffect(
+    shape: Path,
+    advance: Float,
+    phase: Float,
+    style: StampedPathEffectStyle
+): PathEffect
+
+/**
+ * Strategy for transforming each point of the shape along the drawn path
+ *
+ * @sample androidx.compose.ui.graphics.samples.StampedPathEffectSample
+ */
+enum class StampedPathEffectStyle {
+
+    /**
+     * Translate the path shape into the specified location aligning the top left of the path with
+     * the drawn geometry. This does not modify the path itself.
+     *
+     * For example, a circle drawn with a square path and [Translate] will draw the square path
+     * repeatedly with the top left corner of each stamped square along the curvature of the circle.
+     */
+    Translate,
+
+    /**
+     * Rotates the path shape its center along the curvature of the drawn geometry. This does not
+     * modify the path itself.
+     *
+     * For example, a circle drawn with a square path and [Rotate] will draw the square path
+     * repeatedly with the center of each stamped square along the curvature of the circle as well
+     * as each square being rotated along the circumference.
+     */
+    Rotate,
+
+    /**
+     * Modifies the points within the path such that they fit within the drawn geometry. This will
+     * turn straight lines into curves.
+     *
+     * For example, a circle drawn with a square path and [Morph] will modify the straight lines
+     * of the square paths to be curves such that each stamped square is rendered as an arc around
+     * the curvature of the circle.
+     */
+    Morph
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt
index 7fb547f..cf3527d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt
@@ -103,7 +103,7 @@
 fun SweepGradientShader(
     center: Offset,
     colors: List<Color>,
-    colorStops: List<Float>? = null,
+    colorStops: List<Float>? = null
 ): Shader = ActualSweepGradientShader(center, colors, colorStops)
 
 internal expect fun ActualSweepGradientShader(
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
index a73b4a4..45867d0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
@@ -27,10 +27,10 @@
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.graphics.NativePathEffect
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.PaintingStyle
 import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathEffect
 import androidx.compose.ui.graphics.PointMode
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.graphics.StrokeJoin
@@ -102,7 +102,7 @@
         end: Offset,
         strokeWidth: Float,
         cap: StrokeCap,
-        pathEffect: NativePathEffect?,
+        pathEffect: PathEffect?,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -131,7 +131,7 @@
         end: Offset,
         strokeWidth: Float,
         cap: StrokeCap,
-        pathEffect: NativePathEffect?,
+        pathEffect: PathEffect?,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -432,7 +432,7 @@
         color: Color,
         strokeWidth: Float,
         cap: StrokeCap,
-        pathEffect: NativePathEffect?,
+        pathEffect: PathEffect?,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -461,7 +461,7 @@
         brush: Brush,
         strokeWidth: Float,
         cap: StrokeCap,
-        pathEffect: NativePathEffect?,
+        pathEffect: PathEffect?,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -554,15 +554,11 @@
             is Stroke ->
                 obtainStrokePaint()
                     .apply {
-                        with(drawStyle) {
-                            if (strokeWidth != width) strokeWidth = width
-                            if (strokeCap != cap) strokeCap = cap
-                            if (strokeMiterLimit != miter) strokeMiterLimit = miter
-                            if (strokeJoin != join) strokeJoin = join
-
-                            // TODO b/154550525 add PathEffect to Paint if necessary
-                            nativePathEffect = pathEffect
-                        }
+                        if (strokeWidth != drawStyle.width) strokeWidth = drawStyle.width
+                        if (strokeCap != drawStyle.cap) strokeCap = drawStyle.cap
+                        if (strokeMiterLimit != drawStyle.miter) strokeMiterLimit = drawStyle.miter
+                        if (strokeJoin != drawStyle.join) strokeJoin = drawStyle.join
+                        if (pathEffect != drawStyle.pathEffect) pathEffect = drawStyle.pathEffect
                     }
         }
 
@@ -578,7 +574,7 @@
         blendMode: BlendMode
     ): Paint = selectPaint(style).apply {
         if (brush != null) {
-            brush.applyTo(this, alpha)
+            brush.applyTo(size, this, alpha)
         } else if (this.alpha != alpha) {
             this.alpha = alpha
         }
@@ -612,7 +608,7 @@
         miter: Float,
         cap: StrokeCap,
         join: StrokeJoin,
-        pathEffect: NativePathEffect?,
+        pathEffect: PathEffect?,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -629,7 +625,7 @@
             if (this.strokeMiterLimit != miter) this.strokeMiterLimit = miter
             if (this.strokeCap != cap) this.strokeCap = cap
             if (this.strokeJoin != join) this.strokeJoin = join
-            this.nativePathEffect = pathEffect
+            if (this.pathEffect != pathEffect) this.pathEffect = pathEffect
         }
 
     private fun configureStrokePaint(
@@ -638,13 +634,13 @@
         miter: Float,
         cap: StrokeCap,
         join: StrokeJoin,
-        pathEffect: NativePathEffect?,
+        pathEffect: PathEffect?,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
     ) = obtainStrokePaint().apply {
         if (brush != null) {
-            brush.applyTo(this, alpha)
+            brush.applyTo(size, this, alpha)
         } else if (this.alpha != alpha) {
             this.alpha = alpha
         }
@@ -654,7 +650,7 @@
         if (this.strokeMiterLimit != miter) this.strokeMiterLimit = miter
         if (this.strokeCap != cap) this.strokeCap = cap
         if (this.strokeJoin != join) this.strokeJoin = join
-        this.nativePathEffect = pathEffect
+        if (this.pathEffect != pathEffect) this.pathEffect = pathEffect
     }
 
     /**
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
similarity index 60%
copy from car/app/app/src/main/java/androidx/car/app/utils/Logger.java
copy to compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
index a402482..3ce1cd9 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
@@ -13,15 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.car.app.utils;
-
-import androidx.annotation.NonNull;
+package androidx.compose.ui.graphics.drawscope
 
 /**
- * Logger interface to allow the host to log while using the client library.
+ * Receiver scope for drawing content into a layout, where the content can
+ * be drawn between other canvas operations. If [drawContent] is not called,
+ * the contents of the layout will not be drawn.
  */
-// TODO: Allow setting logging severity and including throwables
-public interface Logger {
-    void log(@NonNull String message);
+interface ContentDrawScope : DrawScope {
+    /**
+     * Causes child drawing operations to run during the `onPaint` lambda.
+     */
+    fun drawContent()
 }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
index 16f8b4f..26eeacd 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
@@ -26,9 +26,9 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.NativePathEffect
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathEffect
 import androidx.compose.ui.graphics.PointMode
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.graphics.StrokeJoin
@@ -327,7 +327,7 @@
         end: Offset,
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = Stroke.DefaultCap,
-        pathEffect: NativePathEffect? = null,
+        pathEffect: PathEffect? = null,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -354,7 +354,7 @@
         end: Offset,
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = Stroke.DefaultCap,
-        pathEffect: NativePathEffect? = null,
+        pathEffect: PathEffect? = null,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -745,7 +745,7 @@
         color: Color,
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = StrokeCap.Butt,
-        pathEffect: NativePathEffect? = null,
+        pathEffect: PathEffect? = null,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -773,7 +773,7 @@
         brush: Brush,
         strokeWidth: Float = Stroke.HairlineWidth,
         cap: StrokeCap = StrokeCap.Butt,
-        pathEffect: NativePathEffect? = null,
+        pathEffect: PathEffect? = null,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
         colorFilter: ColorFilter? = null,
         blendMode: BlendMode = DefaultBlendMode
@@ -837,7 +837,7 @@
     /**
      * Effect to apply to the stroke, null indicates a solid stroke line is to be drawn
      */
-    val pathEffect: NativePathEffect? = null
+    val pathEffect: PathEffect? = null
 ) : DrawStyle() {
     companion object {
 
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
index 2f32ba0..996cd0d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
@@ -36,7 +36,6 @@
 import androidx.compose.ui.graphics.vector.PathNode.RelativeReflectiveQuadTo
 import androidx.compose.ui.graphics.vector.PathNode.RelativeVerticalTo
 import androidx.compose.ui.graphics.vector.PathNode.VerticalTo
-import androidx.compose.ui.util.toRadians
 import kotlin.math.PI
 import kotlin.math.abs
 import kotlin.math.atan2
@@ -639,4 +638,7 @@
         var endPosition: Int = 0,
         var endWithNegativeOrDot: Boolean = false
     )
-}
+
+    private fun Float.toRadians(): Float = this / 180f * PI.toFloat()
+    private fun Double.toRadians(): Double = this / 180 * PI
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt
index a22418d..38cc267 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt
@@ -114,9 +114,19 @@
             field = value
         }
 
-    override var nativePathEffect: NativePathEffect? = null
+    override var nativePathEffect: NativePathEffect?
+        get() = pathEffect?.asDesktopPathEffect()
         set(value) {
-            skija.pathEffect = value
+            pathEffect = if (value == null) {
+                null
+            } else {
+                DesktopPathEffect(value)
+            }
+        }
+
+    override var pathEffect: PathEffect? = null
+        set(value) {
+            skija.pathEffect = (value as DesktopPathEffect).asDesktopPathEffect()
             field = value
         }
 
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.kt
new file mode 100644
index 0000000..a443873
--- /dev/null
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import org.jetbrains.skija.PathEffect as SkijaPathEffect
+
+internal class DesktopPathEffect(val nativePathEffect: SkijaPathEffect) : PathEffect
+
+/**
+ * Obtain a reference to the desktop PathEffect type
+ */
+fun PathEffect.asDesktopPathEffect(): SkijaPathEffect =
+    (this as DesktopPathEffect).nativePathEffect
+
+internal actual fun actualCornerPathEffect(radius: Float): PathEffect =
+    DesktopPathEffect(SkijaPathEffect.makeCorner(radius))
+
+internal actual fun actualDashPathEffect(
+    intervals: FloatArray,
+    phase: Float
+): PathEffect = DesktopPathEffect(SkijaPathEffect.makeDash(intervals, phase))
+
+internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
+    DesktopPathEffect(outer.asDesktopPathEffect().makeCompose(inner.asDesktopPathEffect()))
+
+internal actual fun actualStampedPathEffect(
+    shape: Path,
+    advance: Float,
+    phase: Float,
+    style: StampedPathEffectStyle
+): PathEffect =
+    DesktopPathEffect(
+        SkijaPathEffect.makePath1D(
+            shape.asDesktopPath(),
+            advance,
+            phase,
+            style.toSkijaStampedPathEffectStyle()
+        )
+    )
+
+internal fun StampedPathEffectStyle.toSkijaStampedPathEffectStyle(): SkijaPathEffect.Style =
+    when (this) {
+        StampedPathEffectStyle.Morph -> SkijaPathEffect.Style.MORPH
+        StampedPathEffectStyle.Rotate -> SkijaPathEffect.Style.ROTATE
+        StampedPathEffectStyle.Translate -> SkijaPathEffect.Style.TRANSLATE
+    }
\ No newline at end of file
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
index a220afa..8f63900 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
@@ -30,6 +30,7 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiType
 import com.intellij.psi.util.InheritanceUtil
 import org.jetbrains.kotlin.psi.KtCallableDeclaration
@@ -41,6 +42,8 @@
 import org.jetbrains.kotlin.psi.KtUserType
 import org.jetbrains.kotlin.psi.psiUtil.containingClass
 import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.toUElement
+import org.jetbrains.uast.tryResolve
 import java.util.EnumSet
 
 /**
@@ -207,15 +210,26 @@
                 .build()
         )
     } else {
-        val receiverType = (receiverTypeReference.typeElement as KtUserType).referencedName
-        // A receiver that isn't Modifier
-        if (receiverType != ModifierShortName) {
+        val receiverType = (receiverTypeReference.typeElement as KtUserType)
+        val receiverShortName = receiverType.referencedName
+        // Try to resolve the class definition of the receiver
+        val receiverFqn = (
+            receiverType.referenceExpression.toUElement()?.tryResolve().toUElement() as? PsiClass
+            )?.qualifiedName
+        val hasModifierReceiver = if (receiverFqn != null) {
+            // If we could resolve the class, match fqn
+            receiverFqn == ModifierFqn
+        } else {
+            // Otherwise just try and match the short names
+            receiverShortName == ModifierShortName
+        }
+        if (!hasModifierReceiver) {
             report(
                 LintFix.create()
                     .replace()
                     .name("Change receiver to Modifier")
                     .range(context.getLocation(source))
-                    .text(receiverType)
+                    .text(receiverShortName)
                     .with(ModifierShortName)
                     .autoFix()
                     .build()
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/Utils.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/Utils.kt
index 06a57be..89e8f8f 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/Utils.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/Utils.kt
@@ -26,7 +26,6 @@
 val UMethod.isComposable get() = annotations.any { it.qualifiedName == ComposableFqn }
 
 const val ComposableFqn = "androidx.compose.runtime.Composable"
-val ComposableShortName = ComposableFqn.split(".").last()
 
 const val ModifierFqn = "androidx.compose.ui.Modifier"
 val ModifierShortName = ModifierFqn.split(".").last()
\ No newline at end of file
diff --git a/compose/ui/ui-test-font/build.gradle b/compose/ui/ui-test-font/build.gradle
index 16d1675..995fd29 100644
--- a/compose/ui/ui-test-font/build.gradle
+++ b/compose/ui/ui-test-font/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+
+import androidx.build.AndroidXUiPlugin
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.Publish
@@ -25,6 +27,19 @@
     id("AndroidXUiPlugin")
 }
 
+AndroidXUiPlugin.applyAndConfigureKotlinPlugin(project)
+
+if(AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+    kotlin {
+        android()
+        jvm("desktop")
+
+        sourceSets {
+            desktopMain.dependsOn jvmMain
+        }
+    }
+}
+
 androidx {
     name = "Compose Test Font resources"
     publish = Publish.NONE
diff --git a/compose/ui/ui-test-font/src/main/AndroidManifest.xml b/compose/ui/ui-test-font/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-test-font/src/main/AndroidManifest.xml
rename to compose/ui/ui-test-font/src/androidMain/AndroidManifest.xml
diff --git a/compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/invalid_font.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/invalid_font.ttf
diff --git a/compose/ui/ui-test-font/src/main/res/font/kern_font.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/kern_font.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/kern_font.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/kern_font.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/sample_font.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/sample_font.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/sample_font.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/sample_font.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/sample_font2.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/sample_font2.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/sample_font2.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/sample_font2.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_100_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_100_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_100_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_100_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_100_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_100_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_100_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_100_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_200_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_200_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_200_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_200_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_200_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_200_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_200_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_200_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_300_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_300_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_300_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_300_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_300_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_300_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_300_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_300_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_400_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_400_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_400_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_400_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_400_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_400_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_400_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_400_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_500_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_500_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_500_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_500_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_500_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_500_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_500_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_500_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_600_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_600_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_600_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_600_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_600_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_600_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_600_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_600_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_700_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_700_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_700_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_700_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_700_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_700_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_700_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_700_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_800_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_800_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_800_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_800_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_800_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_800_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_800_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_800_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_900_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_900_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_900_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_900_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_900_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_900_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_900_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_900_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index cc099459..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -2,12 +2,12 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
     method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public R getActivityRule();
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
@@ -15,9 +15,11 @@
     method public long getDisplaySize-YbymL2g();
     method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
     method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
+    method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     property public final R activityRule;
     property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
@@ -54,9 +56,11 @@
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
     method public long getDisplaySize-YbymL2g();
+    method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     property public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
     property public abstract androidx.compose.ui.unit.Density density;
@@ -82,16 +86,22 @@
 
 package androidx.compose.ui.test.junit4.android {
 
+  public final class AndroidOwnerRegistryKt {
+  }
+
   public final class ComposeIdlingResourceKt {
-    method public static void registerComposeWithEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
-    method public static void unregisterComposeFromEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void registerComposeWithEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void unregisterComposeFromEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
     ctor public ComposeNotIdleException(String? message, Throwable? cause);
   }
 
+  public final class EspressoLinkKt {
+  }
+
 }
 
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index cc099459..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -2,12 +2,12 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
     method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public R getActivityRule();
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
@@ -15,9 +15,11 @@
     method public long getDisplaySize-YbymL2g();
     method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
     method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
+    method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     property public final R activityRule;
     property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
@@ -54,9 +56,11 @@
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
     method public long getDisplaySize-YbymL2g();
+    method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     property public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
     property public abstract androidx.compose.ui.unit.Density density;
@@ -82,16 +86,22 @@
 
 package androidx.compose.ui.test.junit4.android {
 
+  public final class AndroidOwnerRegistryKt {
+  }
+
   public final class ComposeIdlingResourceKt {
-    method public static void registerComposeWithEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
-    method public static void unregisterComposeFromEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void registerComposeWithEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void unregisterComposeFromEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
     ctor public ComposeNotIdleException(String? message, Throwable? cause);
   }
 
+  public final class EspressoLinkKt {
+  }
+
 }
 
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index cc099459..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -2,12 +2,12 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
     method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public R getActivityRule();
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
@@ -15,9 +15,11 @@
     method public long getDisplaySize-YbymL2g();
     method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
     method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
+    method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     property public final R activityRule;
     property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
@@ -54,9 +56,11 @@
     method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
     method public long getDisplaySize-YbymL2g();
+    method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
     method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
     method public void waitForIdle();
     property public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
     property public abstract androidx.compose.ui.unit.Density density;
@@ -82,16 +86,22 @@
 
 package androidx.compose.ui.test.junit4.android {
 
+  public final class AndroidOwnerRegistryKt {
+  }
+
   public final class ComposeIdlingResourceKt {
-    method public static void registerComposeWithEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
-    method public static void unregisterComposeFromEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void registerComposeWithEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void unregisterComposeFromEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
     ctor public ComposeNotIdleException(String? message, Throwable? cause);
   }
 
+  public final class EspressoLinkKt {
+  }
+
 }
 
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 3c79cab..da98a1d 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -123,7 +123,6 @@
 android {
     tasks.withType(KotlinCompile).configureEach {
         kotlinOptions {
-            freeCompilerArgs += ["-XXLanguage:-NewInference"]
             useIR = true
         }
     }
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
index 4a4d79e..f07b0a2 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
@@ -20,51 +20,46 @@
 import android.view.View
 import android.view.ViewGroup
 import androidx.activity.ComponentActivity
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 
 @LargeTest
 class AndroidOwnerRegistryTest {
 
+    private val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+    private val androidOwnerRegistry = AndroidOwnerRegistry()
+
     @get:Rule
-    val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+    val testRule: RuleChain = RuleChain
+        .outerRule { base, _ -> androidOwnerRegistry.getStatementFor(base) }
+        .around(activityRule)
 
     private val onRegistrationChangedListener =
         object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-            val recordedChanges = mutableListOf<Pair<AndroidOwner, Boolean>>()
-            override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+            val recordedChanges = mutableListOf<Pair<ViewRootForTest, Boolean>>()
+            override fun onRegistrationChanged(owner: ViewRootForTest, registered: Boolean) {
                 recordedChanges.add(Pair(owner, registered))
             }
         }
 
     @Before
     fun setUp() {
-        assertThat(AndroidOwnerRegistry.isSetUp).isFalse()
-        assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
-        AndroidOwnerRegistry.setupRegistry()
-        AndroidOwnerRegistry.addOnRegistrationChangedListener(onRegistrationChangedListener)
-    }
-
-    @After
-    fun tearDown() {
-        AndroidOwnerRegistry.removeOnRegistrationChangedListener(onRegistrationChangedListener)
-        AndroidOwnerRegistry.tearDownRegistry()
-        assertThat(AndroidOwnerRegistry.isSetUp).isFalse()
-        assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+        assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+        androidOwnerRegistry.addOnRegistrationChangedListener(onRegistrationChangedListener)
     }
 
     @Test
     fun registryIsSetUpAndEmpty() {
-        assertThat(AndroidOwnerRegistry.isSetUp).isTrue()
-        assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+        assertThat(androidOwnerRegistry.isSetUp).isTrue()
+        assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
     }
 
     @Test
@@ -72,11 +67,11 @@
         activityRule.scenario.onActivity { activity ->
             // set the composable content and find an owner
             activity.setContent { }
-            val owner = activity.findOwner()
+            val owner = activity.findRootForTest()
 
             // Then it is registered
-            assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEqualTo(setOf(owner))
-            assertThat(AndroidOwnerRegistry.getOwners()).isEqualTo(setOf(owner))
+            assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEqualTo(setOf(owner))
+            assertThat(androidOwnerRegistry.getOwners()).isEqualTo(setOf(owner))
             // And our listener was notified
             assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
                 listOf(Pair(owner, true))
@@ -89,14 +84,14 @@
         activityRule.scenario.onActivity { activity ->
             // set the composable content and find an owner
             activity.setContent { }
-            val owner = activity.findOwner()
+            val owner = activity.findRootForTest()
 
             // And remove it from the hierarchy
             activity.setContentView(View(activity))
 
             // Then it is not registered now
-            assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
-            assertThat(AndroidOwnerRegistry.getOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getOwners()).isEmpty()
             // But our listener was notified of addition and removal
             assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
                 listOf(
@@ -112,14 +107,14 @@
         activityRule.scenario.onActivity { activity ->
             // set the composable content and find an owner
             activity.setContent { }
-            val owner = activity.findOwner()
+            val owner = activity.findRootForTest()
 
             // When we tear down the registry
-            AndroidOwnerRegistry.tearDownRegistry()
+            androidOwnerRegistry.tearDownRegistry()
 
             // Then the registry is empty
-            assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
-            assertThat(AndroidOwnerRegistry.getOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getOwners()).isEmpty()
             // And our listener was notified of addition and removal
             assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
                 listOf(
@@ -131,16 +126,16 @@
     }
 }
 
-private fun Activity.findOwner(): AndroidOwner {
+private fun Activity.findRootForTest(): ViewRootForTest {
     val viewGroup = findViewById<ViewGroup>(android.R.id.content)
-    return requireNotNull(viewGroup.findOwner())
+    return requireNotNull(viewGroup.findRootForTest())
 }
 
-private fun View.findOwner(): AndroidOwner? {
-    if (this is AndroidOwner) return this
+private fun View.findRootForTest(): ViewRootForTest? {
+    if (this is ViewRootForTest) return this
     if (this is ViewGroup) {
         for (i in 0 until childCount) {
-            val owner = getChildAt(i).findOwner()
+            val owner = getChildAt(i).findRootForTest()
             if (owner != null) {
                 return owner
             }
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
index 7bee9b6..c04b95a 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
@@ -16,21 +16,25 @@
 
 package androidx.compose.ui.test.junit4
 
-import android.os.Handler
 import android.os.Looper
+import androidx.activity.ComponentActivity
 import androidx.compose.animation.core.FloatPropKey
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.snap
 import androidx.compose.animation.core.transitionDefinition
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.transition
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.State
+import androidx.compose.runtime.dispatch.withFrameNanos
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
@@ -39,6 +43,12 @@
 import androidx.test.espresso.Espresso.onIdle
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -51,14 +61,12 @@
         private val rectSize = Size(50.0f, 50.0f)
     }
 
-    private val handler = Handler(Looper.getMainLooper())
-
     private var animationRunning = false
     private val recordedAnimatedValues = mutableListOf<Float>()
-    private var hasRecomposed = false
 
     @get:Rule
-    val rule = createComposeRule()
+    val rule = createAndroidComposeRule<ComponentActivity>()
+    private val composeIdlingResource = rule.composeIdlingResource
 
     /**
      * High level test to only verify that [ComposeTestRule.runOnIdle] awaits animations.
@@ -106,86 +114,83 @@
     }
 
     /**
-     * Detailed test to verify if [ComposeIdlingResource.isIdle] reports idleness correctly at
+     * Detailed test to verify if [ComposeIdlingResource.isIdleNow] reports idleness correctly at
      * key moments during the animation kick-off process.
      */
     @Test
+    @Ignore("b/173798666: Idleness not detected after Snapshot.sendApplyNotifications()")
     fun testAnimationIdle_detailed() {
-        var wasIdleAfterCommit = false
-        var wasIdleAfterRecompose = false
         var wasIdleBeforeKickOff = false
-        var wasIdleBeforeCommit = false
+        var wasIdleBeforeApplySnapshot = false
+        var wasIdleAfterApplySnapshot = false
 
         val animationState = mutableStateOf(AnimationStates.From)
-        rule.setContent { Ui(animationState) }
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Ui(animationState)
+        }
 
-        rule.runOnIdle {
-            // Record idleness after this frame is committed. The mutation we're about to make
-            // will trigger a commit of the frame, which is posted at the front of the handler's
-            // queue. By posting a message at the front of the queue here, it will be executed
-            // right after the frame commit.
-            handler.postAtFrontOfQueue {
-                wasIdleAfterCommit = ComposeIdlingResource.isIdle()
-            }
+        runBlocking(scope.coroutineContext) {
+            // Verify that we're on the main thread, which is important for isIdle() later
+            assertThat(Looper.myLooper()).isEqualTo(Looper.getMainLooper())
+        }
 
-            // Record idleness after the next recomposition. Since we can't get a signal from the
-            // recomposer, keep polling until we detect we have been recomposed.
-            hasRecomposed = false
-            handler.pollUntil({ hasRecomposed }) {
-                wasIdleAfterRecompose = ComposeIdlingResource.isIdle()
-            }
-
+        val wasIdleAfterRecompose = rule.runOnIdle {
             // Record idleness before kickoff of animation
-            wasIdleBeforeKickOff = ComposeIdlingResource.isIdle()
+            wasIdleBeforeKickOff = composeIdlingResource.isIdleNow
 
             // Kick off the animation
             animationRunning = true
             animationState.value = AnimationStates.To
 
-            // Record idleness after kickoff of animation, but before the frame is committed
-            wasIdleBeforeCommit = ComposeIdlingResource.isIdle()
+            // Record idleness after kickoff of animation, but before the snapshot is applied
+            wasIdleBeforeApplySnapshot = composeIdlingResource.isIdleNow
+
+            // Apply the snapshot
+            @OptIn(ExperimentalComposeApi::class)
+            Snapshot.sendApplyNotifications()
+
+            // Record idleness after this snapshot is applied
+            wasIdleAfterApplySnapshot = composeIdlingResource.isIdleNow
+
+            // Record idleness after the first recomposition
+            @OptIn(ExperimentalCoroutinesApi::class)
+            scope.async(start = CoroutineStart.UNDISPATCHED) {
+                // Await a single recomposition
+                withFrameNanos {}
+                composeIdlingResource.isIdleNow
+            }
+        }.let {
+            runBlocking {
+                it.await()
+            }
         }
 
-        // Verify that animation is kicked off
-        assertThat(animationRunning).isTrue()
         // Wait until it is finished
-        onIdle()
-        // Verify it was finished
-        assertThat(animationRunning).isFalse()
+        rule.runOnIdle {
+            // Verify it was finished
+            assertThat(animationRunning).isFalse()
 
-        // Before the animation is kicked off, it is still idle
-        assertThat(wasIdleBeforeKickOff).isTrue()
-        // After animation is kicked off, but before the frame is committed, it must be busy
-        assertThat(wasIdleBeforeCommit).isFalse()
-        // After the frame is committed, it must still be busy
-        assertThat(wasIdleAfterCommit).isFalse()
-        // After recomposition, it must still be busy
-        assertThat(wasIdleAfterRecompose).isFalse()
-    }
-
-    private fun Handler.pollUntil(condition: () -> Boolean, onDone: () -> Unit) {
-        object : Runnable {
-            override fun run() {
-                if (condition()) {
-                    onDone()
-                } else {
-                    this@pollUntil.post(this)
-                }
-            }
-        }.run()
+            // Before the animation is kicked off, it is still idle
+            assertThat(wasIdleBeforeKickOff).isTrue()
+            // After animation is kicked off, but before the frame is committed, it must be busy
+            assertThat(wasIdleBeforeApplySnapshot).isFalse()
+            // After the frame is committed, it must still be busy
+            assertThat(wasIdleAfterApplySnapshot).isFalse()
+            // After recomposition, it must still be busy
+            assertThat(wasIdleAfterRecompose).isFalse()
+        }
     }
 
     @Composable
     private fun Ui(animationState: State<AnimationStates>) {
-        hasRecomposed = true
         Box(modifier = Modifier.background(color = Color.Yellow).fillMaxSize()) {
-            hasRecomposed = true
             val state = transition(
                 definition = animationDefinition,
                 toState = animationState.value,
                 onStateChangeFinished = { animationRunning = false }
             )
-            hasRecomposed = true
             Canvas(modifier = Modifier.fillMaxSize()) {
                 recordedAnimatedValues.add(state[x])
                 drawRect(Color.Cyan, Offset(state[x], 0f), rectSize)
@@ -217,4 +222,4 @@
             x using snap()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
new file mode 100644
index 0000000..cde1ff1
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4
+
+import androidx.compose.ui.test.IdlingResource
+import androidx.compose.ui.test.InternalTestingApi
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/*
+ * This class *could* be moved to the test source set, but that makes it more likely to be
+ * skipped if only connectedCheck is run.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class IdlingResourceRegistryTest {
+
+    private var onIdleCalled = false
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val scope = TestCoroutineScope()
+    @OptIn(InternalTestingApi::class)
+    private val registry = IdlingResourceRegistry(scope).apply {
+        setOnIdleCallback { onIdleCalled = true }
+    }
+
+    @After
+    fun verifyRegistryStoppedPolling() {
+        scope.cleanupTestCoroutines()
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleNow_0_IdlingResources() {
+        assertThat(registry.isIdleNow).isTrue()
+        assertNotPolling()
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleNow_1_IdlingResource() {
+        val resource = TestIdlingResource(true)
+        registry.registerIdlingResource(resource)
+        assertThat(registry.isIdleNow).isTrue()
+
+        resource.isIdleNow = false
+        assertThat(registry.isIdleNow).isFalse()
+
+        assertThatPollingStartsAndEnds {
+            resource.isIdleNow = true
+        }
+
+        resource.isIdleNow = false
+        assertThat(registry.isIdleNow).isFalse()
+
+        assertThatPollingStartsAndEnds {
+            resource.isIdleNow = true
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleNow_2_IdlingResources() {
+        val resource1 = TestIdlingResource(true)
+        registry.registerIdlingResource(resource1)
+        val resource2 = TestIdlingResource(true)
+        registry.registerIdlingResource(resource2)
+
+        resource1.isIdleNow = true
+        resource2.isIdleNow = true
+        assertThat(registry.isIdleNow).isTrue()
+
+        resource1.isIdleNow = false
+        resource2.isIdleNow = true
+        assertThat(registry.isIdleNow).isFalse()
+
+        resource1.isIdleNow = true
+        resource2.isIdleNow = false
+        assertThat(registry.isIdleNow).isFalse()
+
+        resource1.isIdleNow = false
+        resource2.isIdleNow = false
+        assertThat(registry.isIdleNow).isFalse()
+
+        assertThatPollingStartsAndEnds {
+            resource1.isIdleNow = true
+            resource2.isIdleNow = true
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleNow_true_doesNotStartPolling() {
+        registry.registerIdlingResource(TestIdlingResource(true))
+        assertThat(registry.isIdleNow).isTrue()
+        assertNotPolling()
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleNow_false_doesNotStartPolling() {
+        registry.registerIdlingResource(TestIdlingResource(false))
+        assertThat(registry.isIdleNow).isFalse()
+        assertNotPolling()
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleOrStartPolling_emptyRegister_doesNotStartPolling() {
+        assertThat(registry.isIdleNow).isTrue()
+        assertThat(registry.isIdleOrEnsurePolling()).isTrue()
+        assertNotPolling()
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleOrStartPolling_idleRegister_doesNotStartPolling() {
+        registry.registerIdlingResource(TestIdlingResource(true))
+        assertThat(registry.isIdleOrEnsurePolling()).isTrue()
+        assertNotPolling()
+    }
+
+    @Test
+    @UiThreadTest
+    fun isIdleOrStartPolling_busyRegister_doesStartPolling() {
+        val resource = TestIdlingResource(false)
+        registry.registerIdlingResource(resource)
+
+        assertThatPollingStartsAndEnds {
+            resource.isIdleNow = true
+        }
+    }
+
+    private fun assertThatPollingStartsAndEnds(makeIdle: () -> Unit) {
+        // Check that we're not polling already ..
+        assertThat(scope.advanceUntilIdle()).isEqualTo(0L)
+        // .. and that we're not idle
+        assertThat(registry.isIdleNow).isFalse()
+
+        // Start the polling
+        onIdleCalled = false
+        assertThat(registry.isIdleOrEnsurePolling()).isFalse()
+
+        // Make the registry idle
+        makeIdle.invoke()
+
+        // Verify that it has polled ..
+        assertThat(scope.advanceUntilIdle()).isGreaterThan(0L)
+        // .. the registry is now idle
+        assertThat(registry.isIdleNow).isTrue()
+        // .. and the onIdle callback was called
+        assertThat(onIdleCalled).isTrue()
+    }
+
+    private fun assertNotPolling() {
+        // Check that no poll job is running ..
+        assertThat(scope.advanceUntilIdle()).isEqualTo(0L)
+        scope.cleanupTestCoroutines()
+        // .. and that the onIdle callback was not called
+        assertThat(onIdleCalled).isFalse()
+    }
+
+    private class TestIdlingResource(initialIdleness: Boolean) : IdlingResource {
+        override var isIdleNow: Boolean = initialIdleness
+    }
+}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
index 77901b8..1ac7476 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
@@ -16,26 +16,41 @@
 
 package androidx.compose.ui.test.junit4
 
+import androidx.activity.ComponentActivity
 import androidx.compose.testutils.expectError
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.test.onNodeWithTag
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.doReturn
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.whenever
+import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
 import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class SynchronizationMethodsTest {
 
-    val rule = createComposeRule()
+    // Note: don't add `@get:Rule` to avoid the Rule from being applied. Except for the
+    // AndroidOwnerRegistry, it doesn't need to be initialized in these tests.
+    private val rule = createAndroidComposeRule<ComponentActivity>()
+    private val androidOwnerRegistry = rule.composeIdlingResource.androidOwnerRegistry
+
+    @get:Rule
+    val registryRule: TestRule = RuleChain.outerRule { base, _ ->
+        androidOwnerRegistry.getStatementFor(base)
+    }
+
+    @Before
+    fun addMockResumedOwner() {
+        androidOwnerRegistry.registerOwner(mockResumedRootForTest())
+    }
 
     @Test
     fun runOnUiThread() {
@@ -58,87 +73,53 @@
 
     @Test
     fun runOnIdle() {
-        withAndroidOwnerRegistry {
-            val result = rule.runOnIdle { "Hello" }
-            assertThat(result).isEqualTo("Hello")
-        }
+        val result = rule.runOnIdle { "Hello" }
+        assertThat(result).isEqualTo("Hello")
     }
 
     @Test
     fun runOnIdle_void() {
-        withAndroidOwnerRegistry {
-            var called = false
-            rule.runOnIdle { called = true }
-            assertThat(called).isTrue()
-        }
+        var called = false
+        rule.runOnIdle { called = true }
+        assertThat(called).isTrue()
     }
 
     @Test
     fun runOnIdle_nullable() {
-        withAndroidOwnerRegistry {
-            val result: String? = rule.runOnIdle { null }
-            assertThat(result).isEqualTo(null)
-        }
+        val result: String? = rule.runOnIdle { null }
+        assertThat(result).isEqualTo(null)
     }
 
     @Test
     fun runOnIdle_assert_fails() {
-        withAndroidOwnerRegistry {
-            rule.runOnIdle {
-                expectError<IllegalStateException> {
-                    rule.onNodeWithTag("dummy").assertExists()
-                }
+        rule.runOnIdle {
+            expectError<IllegalStateException> {
+                rule.onNodeWithTag("dummy").assertExists()
             }
         }
     }
 
     @Test
     fun runOnIdle_waitForIdle_fails() {
-        withAndroidOwnerRegistry {
-            rule.runOnIdle {
-                expectError<IllegalStateException> {
-                    rule.waitForIdle()
-                }
+        rule.runOnIdle {
+            expectError<IllegalStateException> {
+                rule.waitForIdle()
             }
         }
     }
 
     @Test
     fun runOnIdle_runOnIdle_fails() {
-        withAndroidOwnerRegistry {
-            rule.runOnIdle {
-                expectError<IllegalStateException> {
-                    rule.runOnIdle {}
-                }
+        rule.runOnIdle {
+            expectError<IllegalStateException> {
+                rule.runOnIdle {}
             }
         }
     }
 
-    private fun withAndroidOwnerRegistry(block: () -> Unit) {
-        try {
-            AndroidOwnerRegistry.setupRegistry()
-            AndroidOwnerRegistry.registerOwner(mockResumedAndroidOwner())
-            block()
-        } finally {
-            AndroidOwnerRegistry.tearDownRegistry()
-        }
-    }
-
-    private fun mockResumedAndroidOwner(): AndroidOwner {
-        val lifecycle = mock<Lifecycle>()
-        doReturn(Lifecycle.State.RESUMED).whenever(lifecycle).currentState
-
-        val lifecycleOwner = mock<LifecycleOwner>()
-        doReturn(lifecycle).whenever(lifecycleOwner).lifecycle
-
-        val viewTreeOwners = AndroidOwner.ViewTreeOwners(
-            lifecycleOwner = lifecycleOwner,
-            viewModelStoreOwner = mock(),
-            savedStateRegistryOwner = mock()
-        )
-        val owner = mock<AndroidOwner>()
-        doReturn(viewTreeOwners).whenever(owner).viewTreeOwners
-
+    private fun mockResumedRootForTest(): ViewRootForTest {
+        val owner = mock<ViewRootForTest>()
+        doReturn(true).whenever(owner).isLifecycleInResumedState
         return owner
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
index f3ff936..886546d 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
@@ -39,7 +39,6 @@
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.test.ExperimentalTesting
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import androidx.test.espresso.Espresso.onIdle
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
@@ -67,6 +66,7 @@
     @get:Rule
     val rule = createAndroidComposeRule<ComponentActivity>()
     private val clockTestRule = rule.clockTestRule
+    private val composeIdlingResource = rule.composeIdlingResource
 
     /**
      * Tests if advancing the clock manually works when the clock is paused, and that idleness is
@@ -87,7 +87,7 @@
             animationState.value = AnimationStates.To
 
             // Changes need to trickle down the animation system, so compose should be non-idle
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdleNow).isFalse()
         }
 
         // Await recomposition
@@ -101,7 +101,7 @@
         // Advance first half of the animation (.5 sec)
         rule.runOnIdle {
             clockTestRule.advanceClock(halfDuration)
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdleNow).isFalse()
         }
 
         // Await next animation frame
@@ -115,7 +115,7 @@
         // Advance second half of the animation (.5 sec)
         rule.runOnIdle {
             clockTestRule.advanceClock(halfDuration)
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdleNow).isFalse()
         }
 
         // Await next animation frame
@@ -150,7 +150,7 @@
             animationState.value = AnimationStates.To
 
             // Changes need to trickle down the animation system, so compose should be non-idle
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdleNow).isFalse()
         }
 
         // Perform a single recomposition by awaiting the same signal as the Recomposer
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
index 0f7da5e..c417e42 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
@@ -20,10 +20,8 @@
 import androidx.compose.animation.core.rootAnimationClockFactory
 import androidx.compose.ui.test.ExperimentalTesting
 import androidx.compose.ui.test.TestAnimationClock
-import androidx.test.espresso.IdlingResource
-import androidx.compose.ui.test.junit4.android.registerTestClock
-import androidx.compose.ui.test.junit4.android.unregisterTestClock
 import androidx.compose.ui.test.junit4.android.AndroidTestAnimationClock
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -31,30 +29,15 @@
 /**
  * A [TestRule] to monitor and take over the animation clock in the composition. It substitutes
  * the ambient animation clock provided at the root of the composition tree with a
- * [TestAnimationClock] and registers it with [registerTestClock].
- *
- * Usually you don't need to create this rule by yourself, it is done for you in
- * [ComposeTestRule]. If you don't use [ComposeTestRule], use this rule in your test and make
- * sure it is run _before_ your activity is created.
- *
- * If your app provides a custom animation clock somewhere in your composition, make sure to have
- * it implement [TestAnimationClock] and register it with [registerTestClock]. Alternatively,
- * if you use Espresso you can create your own [IdlingResource] to let Espresso await your
- * animations. Otherwise, built in steps that make sure the UI is stable when performing actions
- * or assertions will fail to work.
+ * [TestAnimationClock].
  */
 @ExperimentalTesting
-internal class AndroidAnimationClockTestRule : AnimationClockTestRule {
+internal class AndroidAnimationClockTestRule(
+    private val composeIdlingResource: ComposeIdlingResource
+) : AnimationClockTestRule {
 
     /** Backing property for [clock] */
     private val _clock = AndroidTestAnimationClock()
-
-    /**
-     * The ambient animation clock that is provided at the root of the composition tree.
-     * Typically, apps will only use this clock. If your app provides another clock in the tree,
-     * make sure to let it implement [TestAnimationClock] and register it with
-     * [registerTestClock].
-     */
     override val clock: TestAnimationClock get() = _clock
 
     override fun apply(base: Statement, description: Description?): Statement {
@@ -65,7 +48,7 @@
     private inner class AnimationClockStatement(private val base: Statement) : Statement() {
         override fun evaluate() {
             val oldFactory = rootAnimationClockFactory
-            registerTestClock(clock)
+            composeIdlingResource.registerTestClock(clock)
             rootAnimationClockFactory = { clock }
             try {
                 base.evaluate()
@@ -74,13 +57,20 @@
                     _clock.dispose()
                 } finally {
                     rootAnimationClockFactory = oldFactory
-                    unregisterTestClock(clock)
+                    composeIdlingResource.unregisterTestClock(clock)
                 }
             }
         }
     }
 }
 
+@Deprecated(
+    message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+        "it from your ComposeTestRule instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
 @ExperimentalTesting
+@Suppress("DocumentExceptions")
 actual fun createAnimationClockRule(): AnimationClockTestRule =
-    AndroidAnimationClockTestRule()
+    throw UnsupportedOperationException()
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
index 8f8fe80..572d8c2 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
@@ -17,21 +17,20 @@
 package androidx.compose.ui.test.junit4
 
 import androidx.activity.ComponentActivity
+import androidx.annotation.VisibleForTesting
 import androidx.compose.foundation.text.blinkingCursorEnabled
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Recomposer
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.InternalTestingApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
 import androidx.compose.ui.test.createTestContext
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
-import androidx.compose.ui.test.junit4.android.FirstDrawRegistry
-import androidx.compose.ui.test.junit4.android.IdleAwaiter
-import androidx.compose.ui.test.junit4.android.registerComposeWithEspresso
-import androidx.compose.ui.test.junit4.android.unregisterComposeFromEspresso
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
+import androidx.compose.ui.test.junit4.android.EspressoLink
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.input.textInputServiceFactory
 import androidx.compose.ui.unit.Density
@@ -141,18 +140,25 @@
         activityProvider: (R) -> A
     ) : this(activityRule, activityProvider, false)
 
+    private val idlingResourceRegistry = IdlingResourceRegistry()
+    private val espressoLink = EspressoLink(idlingResourceRegistry)
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal val composeIdlingResource = ComposeIdlingResource().also {
+        registerIdlingResource(it)
+    }
+
     @ExperimentalTesting
     override val clockTestRule: AnimationClockTestRule =
         if (!driveClockByMonotonicFrameClock) {
-            AndroidAnimationClockTestRule()
+            AndroidAnimationClockTestRule(composeIdlingResource)
         } else {
-            MonotonicFrameClockTestRule()
+            MonotonicFrameClockTestRule(composeIdlingResource)
         }
 
     internal var disposeContentHook: (() -> Unit)? = null
 
-    private val idleAwaiter = IdleAwaiter()
-    private val testOwner = AndroidTestOwner(idleAwaiter)
+    private val testOwner = AndroidTestOwner(composeIdlingResource)
     private val testContext = createTestContext(testOwner)
 
     private var activity: A? = null
@@ -175,11 +181,14 @@
         }
     }
 
-    override fun apply(base: Statement, description: Description?): Statement {
+    override fun apply(base: Statement, description: Description): Statement {
         @Suppress("NAME_SHADOWING")
         @OptIn(ExperimentalTesting::class)
         return RuleChain
-            .outerRule(clockTestRule)
+            .outerRule { base, _ -> composeIdlingResource.getStatementFor(base) }
+            .around { base, _ -> idlingResourceRegistry.getStatementFor(base) }
+            .around { base, _ -> espressoLink.getStatementFor(base) }
+            .around(clockTestRule)
             .around { base, _ -> AndroidComposeStatement(base) }
             .around(activityRule)
             .apply(base, description)
@@ -216,12 +225,12 @@
     }
 
     override fun waitForIdle() {
-        idleAwaiter.waitForIdle()
+        composeIdlingResource.waitForIdle()
     }
 
     @ExperimentalTesting
     override suspend fun awaitIdle() {
-        idleAwaiter.awaitIdle()
+        composeIdlingResource.awaitIdle()
     }
 
     override fun <T> runOnUiThread(action: () -> T): T {
@@ -235,6 +244,14 @@
         return runOnUiThread(action)
     }
 
+    override fun registerIdlingResource(idlingResource: IdlingResource) {
+        idlingResourceRegistry.registerIdlingResource(idlingResource)
+    }
+
+    override fun unregisterIdlingResource(idlingResource: IdlingResource) {
+        idlingResourceRegistry.unregisterIdlingResource(idlingResource)
+    }
+
     inner class AndroidComposeStatement(
         private val base: Statement
     ) : Statement() {
@@ -242,54 +259,38 @@
         override fun evaluate() {
             @Suppress("DEPRECATION_ERROR")
             val oldTextInputFactory = textInputServiceFactory
-            beforeEvaluate()
             try {
+                @Suppress("DEPRECATION_ERROR")
+                blinkingCursorEnabled = false
+                @Suppress("DEPRECATION_ERROR")
+                textInputServiceFactory = {
+                    TextInputServiceForTests(it)
+                }
                 base.evaluate()
             } finally {
-                afterEvaluate()
+                @Suppress("DEPRECATION_ERROR")
+                blinkingCursorEnabled = true
                 @Suppress("DEPRECATION_ERROR")
                 textInputServiceFactory = oldTextInputFactory
-            }
-        }
-
-        @OptIn(InternalTextApi::class)
-        private fun beforeEvaluate() {
-            @Suppress("DEPRECATION_ERROR")
-            blinkingCursorEnabled = false
-            AndroidOwnerRegistry.setupRegistry()
-            FirstDrawRegistry.setupRegistry()
-            registerComposeWithEspresso()
-            @Suppress("DEPRECATION_ERROR")
-            textInputServiceFactory = {
-                TextInputServiceForTests(it)
-            }
-        }
-
-        @OptIn(InternalTextApi::class)
-        private fun afterEvaluate() {
-            @Suppress("DEPRECATION_ERROR")
-            blinkingCursorEnabled = true
-            AndroidOwnerRegistry.tearDownRegistry()
-            FirstDrawRegistry.tearDownRegistry()
-            unregisterComposeFromEspresso()
-            // Dispose the content
-            if (disposeContentHook != null) {
-                runOnUiThread {
-                    // NOTE: currently, calling dispose after an exception that happened during
-                    // composition is not a safe call. Compose runtime should fix this, and then
-                    // this call will be okay. At the moment, however, calling this could
-                    // itself produce an exception which will then obscure the original
-                    // exception. To fix this, we will just wrap this call in a try/catch of
-                    // its own
-                    try {
-                        disposeContentHook!!()
-                    } catch (e: Exception) {
-                        // ignore
+                // Dispose the content
+                if (disposeContentHook != null) {
+                    runOnUiThread {
+                        // NOTE: currently, calling dispose after an exception that happened during
+                        // composition is not a safe call. Compose runtime should fix this, and then
+                        // this call will be okay. At the moment, however, calling this could
+                        // itself produce an exception which will then obscure the original
+                        // exception. To fix this, we will just wrap this call in a try/catch of
+                        // its own
+                        try {
+                            disposeContentHook!!()
+                        } catch (e: Exception) {
+                            // ignore
+                        }
+                        disposeContentHook = null
                     }
-                    disposeContentHook = null
                 }
+                activity = null
             }
-            activity = null
         }
     }
 
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
index 19e890a..ded27d1 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
@@ -17,24 +17,23 @@
 package androidx.compose.ui.test.junit4
 
 import android.annotation.SuppressLint
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.test.InternalTestingApi
 import androidx.compose.ui.test.TestOwner
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
-import androidx.compose.ui.test.junit4.android.IdleAwaiter
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import androidx.compose.ui.text.input.EditOperation
 import androidx.compose.ui.text.input.ImeAction
 
 @OptIn(InternalTestingApi::class)
-internal class AndroidTestOwner(private val idleAwaiter: IdleAwaiter) : TestOwner {
+internal class AndroidTestOwner(
+    private val composeIdlingResource: ComposeIdlingResource
+) : TestOwner {
 
     @SuppressLint("DocumentExceptions")
     override fun sendTextInputCommand(node: SemanticsNode, command: List<EditOperation>) {
-        @OptIn(ExperimentalLayoutNodeApi::class)
-        val owner = node.componentNode.owner as AndroidOwner
+        val owner = node.owner as ViewRootForTest
 
         @Suppress("DEPRECATION")
         runOnUiThread {
@@ -47,8 +46,7 @@
 
     @SuppressLint("DocumentExceptions")
     override fun sendImeAction(node: SemanticsNode, actionSpecified: ImeAction) {
-        @OptIn(ExperimentalLayoutNodeApi::class)
-        val owner = node.componentNode.owner as AndroidOwner
+        val owner = node.owner as ViewRootForTest
 
         @Suppress("DEPRECATION")
         runOnUiThread {
@@ -65,19 +63,10 @@
     }
 
     override fun getOwners(): Set<Owner> {
-        // TODO(pavlis): Instead of returning a flatMap, let all consumers handle a tree
-        //  structure. In case of multiple AndroidOwners, add a fake root
-        idleAwaiter.waitForIdle()
-
-        return AndroidOwnerRegistry.getOwners().also {
-            // TODO(b/153632210): This check should be done by callers of collectOwners
-            check(it.isNotEmpty()) {
-                "No compose views found in the app. Is your Activity resumed?"
-            }
-        }
+        return composeIdlingResource.getOwners()
     }
 
-    private fun AndroidOwner.getTextInputServiceOrDie(): TextInputServiceForTests {
+    private fun ViewRootForTest.getTextInputServiceOrDie(): TextInputServiceForTests {
         return textInputService as? TextInputServiceForTests
             ?: throw IllegalStateException(
                 "Text input service wrapper not set up! Did you use ComposeTestRule?"
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
index ac434d4..8aa2f09 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
@@ -22,14 +22,15 @@
 import androidx.compose.animation.core.rootAnimationClockFactory
 import androidx.compose.ui.test.ExperimentalTesting
 import androidx.compose.ui.test.TestAnimationClock
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource.registerTestClock
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource.unregisterTestClock
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import kotlinx.coroutines.CoroutineScope
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
 @ExperimentalTesting
-internal class MonotonicFrameClockTestRule : AnimationClockTestRule {
+internal class MonotonicFrameClockTestRule(
+    private val composeIdlingResource: ComposeIdlingResource
+) : AnimationClockTestRule {
 
     private lateinit var _clock: InternalClock
     override val clock: TestAnimationClock get() = _clock
@@ -41,7 +42,7 @@
     private fun getOrCreateClock(scope: CoroutineScope): TestAnimationClock {
         if (!this::_clock.isInitialized) {
             _clock = InternalClock(MonotonicFrameAnimationClock(scope))
-            registerTestClock(_clock)
+            composeIdlingResource.registerTestClock(_clock)
         }
         return _clock
     }
@@ -55,7 +56,7 @@
                 base.evaluate()
             } finally {
                 rootAnimationClockFactory = oldFactory
-                unregisterTestClock(clock)
+                composeIdlingResource.unregisterTestClock(clock)
             }
         }
     }
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
index ae51818..0de743a 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
@@ -17,37 +17,47 @@
 package androidx.compose.ui.test.junit4.android
 
 import android.view.View
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.lifecycle.Lifecycle
+import androidx.annotation.VisibleForTesting
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.test.ExperimentalTesting
+import kotlinx.coroutines.suspendCancellableCoroutine
+import org.junit.runners.model.Statement
 import java.util.Collections
 import java.util.WeakHashMap
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.resume
+import kotlin.time.ExperimentalTime
 
 /**
- * Registry where all [AndroidOwner]s should be registered while they are attached to the window.
- * This registry is used by the testing library to query the owners's state.
+ * Registry where all views implementing [ViewRootForTest] should be registered while they
+ * are attached to the window. This registry is used by the testing library to query the owners's
+ * state.
  */
-internal object AndroidOwnerRegistry {
-    private val owners = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
+internal class AndroidOwnerRegistry {
+    private val owners = Collections.newSetFromMap(WeakHashMap<ViewRootForTest, Boolean>())
     private val registryListeners = mutableSetOf<OnRegistrationChangedListener>()
 
     /**
-     * Returns if the registry is setup to receive registrations from [AndroidOwner]s
+     * Returns if the registry is setup to receive registrations from [ViewRootForTest]s
      */
     val isSetUp: Boolean
-        get() = AndroidOwner.onAndroidOwnerCreatedCallback == ::onAndroidOwnerCreated
+        get() = ViewRootForTest.onViewCreatedCallback == ::onComposeViewCreated
 
     /**
-     * Sets up this registry to be notified of any [AndroidOwner] created
+     * Sets up this registry to be notified of any [ViewRootForTest] created
      */
-    internal fun setupRegistry() {
-        AndroidOwner.onAndroidOwnerCreatedCallback = ::onAndroidOwnerCreated
+    private fun setupRegistry() {
+        ViewRootForTest.onViewCreatedCallback = ::onComposeViewCreated
     }
 
     /**
      * Cleans up the changes made by [setupRegistry]. Call this after your test has run.
      */
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal fun tearDownRegistry() {
-        AndroidOwner.onAndroidOwnerCreatedCallback = null
+        ViewRootForTest.onViewCreatedCallback = null
         synchronized(owners) {
             getUnfilteredOwners().forEach {
                 unregisterOwner(it)
@@ -57,33 +67,32 @@
         }
     }
 
-    private fun onAndroidOwnerCreated(owner: AndroidOwner) {
+    private fun onComposeViewCreated(owner: ViewRootForTest) {
         owner.view.addOnAttachStateChangeListener(OwnerAttachedListener(owner))
     }
 
     /**
-     * Returns a copy of the set of all registered [AndroidOwner]s, including ones that are
+     * Returns a copy of the set of all registered [ViewRootForTest]s, including ones that are
      * normally not relevant (like those whose lifecycle state is not RESUMED).
      */
-    fun getUnfilteredOwners(): Set<AndroidOwner> {
+    fun getUnfilteredOwners(): Set<ViewRootForTest> {
         return synchronized(owners) { owners.toSet() }
     }
 
     /**
-     * Returns a copy of the set of all registered [AndroidOwner]s that can be interacted with.
+     * Returns a copy of the set of all registered [ViewRootForTest]s that can be interacted with.
      * This method is almost always preferred over [getUnfilteredOwners].
      */
-    fun getOwners(): Set<AndroidOwner> {
+    fun getOwners(): Set<ViewRootForTest> {
         return synchronized(owners) {
             owners.filterTo(mutableSetOf()) {
-                it.viewTreeOwners?.lifecycleOwner
-                    ?.lifecycle?.currentState == Lifecycle.State.RESUMED
+                it.isLifecycleInResumedState
             }
         }
     }
 
     /**
-     * Adds the given [listener], to be notified when an [AndroidOwner] registers or unregisters.
+     * Adds the given [listener], to be notified when an [ViewRootForTest] registers or unregisters.
      */
     fun addOnRegistrationChangedListener(listener: OnRegistrationChangedListener) {
         synchronized(registryListeners) { registryListeners.add(listener) }
@@ -96,7 +105,7 @@
         synchronized(registryListeners) { registryListeners.remove(listener) }
     }
 
-    private fun dispatchOnRegistrationChanged(owner: AndroidOwner, isRegistered: Boolean) {
+    private fun dispatchOnRegistrationChanged(owner: ViewRootForTest, isRegistered: Boolean) {
         synchronized(registryListeners) { registryListeners.toList() }.forEach {
             it.onRegistrationChanged(owner, isRegistered)
         }
@@ -105,7 +114,7 @@
     /**
      * Registers the [owner] in this registry. Must be called from [View.onAttachedToWindow].
      */
-    internal fun registerOwner(owner: AndroidOwner) {
+    internal fun registerOwner(owner: ViewRootForTest) {
         synchronized(owners) {
             if (owners.add(owner)) {
                 dispatchOnRegistrationChanged(owner, true)
@@ -116,7 +125,7 @@
     /**
      * Unregisters the [owner] from this registry. Must be called from [View.onDetachedFromWindow].
      */
-    internal fun unregisterOwner(owner: AndroidOwner) {
+    internal fun unregisterOwner(owner: ViewRootForTest) {
         synchronized(owners) {
             if (owners.remove(owner)) {
                 dispatchOnRegistrationChanged(owner, false)
@@ -124,16 +133,29 @@
         }
     }
 
+    fun getStatementFor(base: Statement): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                try {
+                    setupRegistry()
+                    base.evaluate()
+                } finally {
+                    tearDownRegistry()
+                }
+            }
+        }
+    }
+
     /**
-     * Interface to be implemented by components that want to be notified when an [AndroidOwner]
+     * Interface to be implemented by components that want to be notified when an [ViewRootForTest]
      * registers or unregisters at this registry.
      */
     interface OnRegistrationChangedListener {
-        fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean)
+        fun onRegistrationChanged(owner: ViewRootForTest, registered: Boolean)
     }
 
-    private class OwnerAttachedListener(
-        private val owner: AndroidOwner
+    private inner class OwnerAttachedListener(
+        private val owner: ViewRootForTest
     ) : View.OnAttachStateChangeListener {
 
         // Note: owner.view === view, because the owner _is_ the view,
@@ -147,4 +169,78 @@
             unregisterOwner(owner)
         }
     }
-}
\ No newline at end of file
+}
+
+private val AndroidOwnerRegistry.hasAndroidOwners: Boolean get() = getOwners().isNotEmpty()
+
+private fun AndroidOwnerRegistry.ensureAndroidOwnerRegistryIsSetUp() {
+    check(isSetUp) {
+        "Test not setup properly. Use a ComposeTestRule in your test to be able to interact " +
+            "with composables"
+    }
+}
+
+internal fun AndroidOwnerRegistry.waitForAndroidOwners() {
+    ensureAndroidOwnerRegistryIsSetUp()
+
+    if (!hasAndroidOwners) {
+        val latch = CountDownLatch(1)
+        val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
+            override fun onRegistrationChanged(owner: ViewRootForTest, registered: Boolean) {
+                if (hasAndroidOwners) {
+                    latch.countDown()
+                }
+            }
+        }
+        try {
+            addOnRegistrationChangedListener(listener)
+            if (!hasAndroidOwners) {
+                latch.await(2, TimeUnit.SECONDS)
+            }
+        } finally {
+            removeOnRegistrationChangedListener(listener)
+        }
+    }
+}
+
+@ExperimentalTesting
+@OptIn(ExperimentalTime::class)
+internal suspend fun AndroidOwnerRegistry.awaitAndroidOwners() {
+    ensureAndroidOwnerRegistryIsSetUp()
+
+    if (!hasAndroidOwners) {
+        suspendCancellableCoroutine<Unit> { continuation ->
+            // Make sure we only resume once
+            val didResume = AtomicBoolean(false)
+            fun resume(listener: AndroidOwnerRegistry.OnRegistrationChangedListener) {
+                if (didResume.compareAndSet(false, true)) {
+                    removeOnRegistrationChangedListener(listener)
+                    continuation.resume(Unit)
+                }
+            }
+
+            // Usually we resume if an ComposeViewTestMarker is registered while the listener is added
+            val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
+                override fun onRegistrationChanged(
+                    owner: ViewRootForTest,
+                    registered: Boolean
+                ) {
+                    if (hasAndroidOwners) {
+                        resume(this)
+                    }
+                }
+            }
+
+            addOnRegistrationChangedListener(listener)
+            continuation.invokeOnCancellation {
+                removeOnRegistrationChangedListener(listener)
+            }
+
+            // Sometimes the ComposeViewTestMarker was registered before we added
+            // the listener, in which case we missed our signal
+            if (hasAndroidOwners) {
+                resume(listener)
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
index d792d7c..e99a0a8 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
@@ -16,27 +16,19 @@
 
 package androidx.compose.ui.test.junit4.android
 
-import android.os.Handler
-import android.os.Looper
+import androidx.annotation.VisibleForTesting
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.Recomposer
 import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.node.Owner
 import androidx.compose.ui.test.ExperimentalTesting
-import androidx.test.espresso.IdlingRegistry
-import androidx.test.espresso.IdlingResource
+import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.TestAnimationClock
-import androidx.compose.ui.test.junit4.runOnUiThread
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicInteger
-
-/**
- * In case Espresso times out, implementing this interface enables our resources to explain why
- * they failed to synchronize in case they were busy.
- */
-internal interface IdlingResourceWithDiagnostics {
-    // TODO: Consider this as a public API.
-    fun getDiagnosticMessageIfBusy(): String?
-}
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.isOnUiThread
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.junit.runners.model.Statement
 
 /**
  * Register compose's idling check to Espresso.
@@ -46,136 +38,119 @@
  * [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
  * wait for Compose being idle you can use this function.
  */
-fun registerComposeWithEspresso() {
-    ComposeIdlingResource.registerSelfIntoEspresso()
-    FirstDrawIdlingResource.registerSelfIntoEspresso()
-}
+@Deprecated(
+    message = "Global (un)registration of ComposeIdlingResource is no longer supported. Use " +
+        "createAndroidComposeRule() and registration will happen when needed",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("")
+)
+@Suppress("DocumentExceptions")
+fun registerComposeWithEspresso(): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of ComposeIdlingResource is no longer supported"
+)
 
 /**
  * Unregisters resource registered as part of [registerComposeWithEspresso].
  */
-fun unregisterComposeFromEspresso() {
-    ComposeIdlingResource.unregisterSelfFromEspresso()
-    FirstDrawIdlingResource.unregisterSelfFromEspresso()
-}
+@Deprecated(
+    message = "Global (un)registration of ComposeIdlingResource is no longer supported. Use " +
+        "createAndroidComposeRule() and registration will happen when needed",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("")
+)
+@Suppress("DocumentExceptions")
+fun unregisterComposeFromEspresso(): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of ComposeIdlingResource is no longer supported"
+)
 
 /**
  * Registers the given [clock] so Espresso can await the animations subscribed to that clock.
  */
+@Deprecated(
+    message = "Global (un)registration of TestAnimationClocks is no longer supported. Use the " +
+        "member function ComposeIdlingResource.registerTestClock(clock) instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeIdlingResource.registerTestClock(clock)")
+)
 @ExperimentalTesting
-fun registerTestClock(clock: TestAnimationClock) {
-    ComposeIdlingResource.registerTestClock(clock)
-}
+@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
+fun registerTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
+        "directly on an instance of ComposeIdlingResource instead"
+)
 
 /**
  * Unregisters the [clock] that was registered with [registerTestClock].
  */
+@Deprecated(
+    message = "Global (un)registration of TestAnimationClocks is no longer supported. Use the " +
+        "member function ComposeIdlingResource.unregisterTestClock(clock) instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeIdlingResource.unregisterTestClock(clock)")
+)
 @ExperimentalTesting
-fun unregisterTestClock(clock: TestAnimationClock) {
-    ComposeIdlingResource.unregisterTestClock(clock)
-}
+@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
+fun unregisterTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
+        "directly on an instance of ComposeIdlingResource instead"
+)
 
 /**
  * Provides an idle check to be registered into Espresso.
  *
  * This makes sure that Espresso is able to wait for any pending changes in Compose. This
  * resource is automatically registered when any compose testing APIs are used including
- * [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
- * wait for Compose being idle you can register this yourself via [registerSelfIntoEspresso].
+ * [createAndroidComposeRule].
  */
-internal object ComposeIdlingResource : BaseIdlingResource(), IdlingResourceWithDiagnostics {
+internal class ComposeIdlingResource : IdlingResource {
 
-    override fun getName(): String = "ComposeIdlingResource"
-
-    private var isIdleCheckScheduled = false
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal val androidOwnerRegistry = AndroidOwnerRegistry()
 
     @OptIn(ExperimentalTesting::class)
     private val clocks = mutableSetOf<TestAnimationClock>()
 
-    private val handler = Handler(Looper.getMainLooper())
-
     private var hadAnimationClocksIdle = true
     private var hadNoSnapshotChanges = true
     private var hadNoRecomposerChanges = true
-    private var lastCompositionAwaiters = 0
-
-    private var compositionAwaiters = AtomicInteger(0)
+    private var hadNoPendingMeasureLayout = true
+    // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//    private var hadNoPendingDraw = true
 
     /**
-     * Returns whether or not Compose is idle, without starting to poll if it is not.
+     * Returns whether or not Compose is idle now.
      */
-    @OptIn(ExperimentalComposeApi::class)
-    fun isIdle(): Boolean {
-        @Suppress("DEPRECATION")
-        return runOnUiThread {
+    override val isIdleNow: Boolean
+        @OptIn(ExperimentalComposeApi::class)
+        get() {
             hadNoSnapshotChanges = !Snapshot.current.hasPendingChanges()
             hadNoRecomposerChanges = !Recomposer.current().hasInvalidations()
             hadAnimationClocksIdle = areAllClocksIdle()
-            lastCompositionAwaiters = compositionAwaiters.get()
+            val owners = androidOwnerRegistry.getUnfilteredOwners()
+            hadNoPendingMeasureLayout = !owners.any { it.hasPendingMeasureOrLayout }
+            // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//            hadNoPendingDraw = !owners.any {
+//                val hasContent = it.view.measuredWidth != 0 && it.view.measuredHeight != 0
+//                it.view.isDirty && (hasContent || it.view.isLayoutRequested)
+//            }
 
-            check(lastCompositionAwaiters >= 0) {
-                "More CompositionAwaiters were removed then added ($lastCompositionAwaiters)"
-            }
-
-            hadNoSnapshotChanges && hadNoRecomposerChanges && hadAnimationClocksIdle &&
-                lastCompositionAwaiters == 0
+            return hadNoSnapshotChanges &&
+                hadNoRecomposerChanges &&
+                hadAnimationClocksIdle &&
+                // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+                hadNoPendingMeasureLayout /*&&
+                hadNoPendingDraw*/
         }
-    }
-
-    /**
-     * Returns whether or not Compose is idle, and starts polling if it is not. Will always be
-     * called from the main thread by Espresso, and should _only_ be called from Espresso. Use
-     * [isIdle] if you need to query the idleness of Compose manually.
-     */
-    override fun isIdleNow(): Boolean {
-        val isIdle = isIdle()
-        if (!isIdle) {
-            scheduleIdleCheck()
-        }
-        return isIdle
-    }
-
-    private fun scheduleIdleCheck() {
-        if (!isIdleCheckScheduled) {
-            isIdleCheckScheduled = true
-            handler.postDelayed(
-                {
-                    isIdleCheckScheduled = false
-                    if (isIdle()) {
-                        transitionToIdle()
-                    } else {
-                        scheduleIdleCheck()
-                    }
-                }, /* delayMillis = */ 20
-            )
-        }
-    }
-
-    /**
-     * Called by [CompositionAwaiter] to indicate that this [ComposeIdlingResource] should report
-     * busy to Espresso while that [CompositionAwaiter] is checking idleness.
-     */
-    internal fun addCompositionAwaiter() {
-        compositionAwaiters.incrementAndGet()
-    }
-
-    /**
-     * Called by [CompositionAwaiter] to indicate that this [ComposeIdlingResource] can report
-     * idle as far as the calling [CompositionAwaiter] is concerned.
-     */
-    internal fun removeCompositionAwaiter() {
-        compositionAwaiters.decrementAndGet()
-    }
 
     @OptIn(ExperimentalTesting::class)
-    internal fun registerTestClock(clock: TestAnimationClock) {
+    fun registerTestClock(clock: TestAnimationClock) {
         synchronized(clocks) {
             clocks.add(clock)
         }
     }
 
     @OptIn(ExperimentalTesting::class)
-    internal fun unregisterTestClock(clock: TestAnimationClock) {
+    fun unregisterTestClock(clock: TestAnimationClock) {
         synchronized(clocks) {
             clocks.remove(clock)
         }
@@ -192,11 +167,11 @@
         val hadSnapshotChanges = !hadNoSnapshotChanges
         val hadRecomposerChanges = !hadNoRecomposerChanges
         val hadRunningAnimations = !hadAnimationClocksIdle
-        val numCompositionAwaiters = lastCompositionAwaiters
-        val wasAwaitingCompositions = numCompositionAwaiters > 0
+        val hadPendingMeasureLayout = !hadNoPendingMeasureLayout
+        // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//        val hadPendingDraw = !hadNoPendingDraw
 
-        val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges &&
-            !hadRunningAnimations && !wasAwaitingCompositions
+        val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges && !hadRunningAnimations
 
         if (wasIdle) {
             return null
@@ -206,71 +181,74 @@
         if (hadRunningAnimations) {
             busyReasons.add("animations")
         }
-        val busyRecomposing = hadSnapshotChanges || hadRecomposerChanges || wasAwaitingCompositions
+        val busyRecomposing = hadSnapshotChanges || hadRecomposerChanges
         if (busyRecomposing) {
             busyReasons.add("pending recompositions")
         }
 
-        var message = "$name is busy due to ${busyReasons.joinToString(", ")}.\n"
+        var message = "${javaClass.simpleName} is busy due to ${busyReasons.joinToString(", ")}.\n"
         if (busyRecomposing) {
             message += "- Note: Timeout on pending recomposition means that there are most likely" +
                 " infinite re-compositions happening in the tested code.\n"
             message += "- Debug: hadRecomposerChanges = $hadRecomposerChanges, "
             message += "hadSnapshotChanges = $hadSnapshotChanges, "
-            message += "numCompositionAwaiters = $numCompositionAwaiters"
+            message += "hadPendingMeasureLayout = $hadPendingMeasureLayout"
+            // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//            message += ", hadPendingDraw = $hadPendingDraw"
         }
         return message
     }
-}
 
-private object FirstDrawIdlingResource : BaseIdlingResource() {
-    override fun getName(): String = "FirstDrawIdlingResource"
+    fun waitForIdle() {
+        check(!isOnUiThread()) {
+            "Functions that involve synchronization (Assertions, Actions, Synchronization; " +
+                "e.g. assertIsSelected(), doClick(), runOnIdle()) cannot be run " +
+                "from the main thread. Did you nest such a function inside " +
+                "runOnIdle {}, runOnUiThread {} or setContent {}?"
+        }
 
-    override fun isIdleNow(): Boolean {
-        return FirstDrawRegistry.haveAllDrawn().also {
-            if (!it) {
-                FirstDrawRegistry.setOnDrawnCallback(::transitionToIdle)
+        // First wait until we have an AndroidOwner (in case an Activity is being started)
+        androidOwnerRegistry.waitForAndroidOwners()
+        // Then await composition(s)
+        runEspressoOnIdle()
+
+        // TODO(b/155774664): waitForAndroidOwners() may be satisfied by an AndroidOwner from an
+        //  Activity that is about to be paused, in cases where a new Activity is being started.
+        //  That means that AndroidOwnerRegistry.getOwners() may still return an empty list
+        //  between now and when the new Activity has created its AndroidOwner, even though
+        //  waitForAndroidOwners() suggests that we are now guaranteed one.
+    }
+
+    @ExperimentalTesting
+    suspend fun awaitIdle() {
+        // TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
+        //  runEspressoOnIdle() and replace it with a suspend fun that loops while the
+        //  snapshot or the recomposer has pending changes, clocks are busy or owners have
+        //  pending measures or layouts; and do the await on AndroidUiDispatcher.Main
+        // We use Espresso to wait for composition, measure, layout and draw,
+        // and Espresso needs to be called from a non-ui thread; so use Dispatchers.IO
+        withContext(Dispatchers.IO) {
+            // First wait until we have an AndroidOwner (in case an Activity is being started)
+            androidOwnerRegistry.awaitAndroidOwners()
+            // Then await composition(s)
+            runEspressoOnIdle()
+        }
+    }
+
+    fun getOwners(): Set<Owner> {
+        // TODO(pavlis): Instead of returning a flatMap, let all consumers handle a tree
+        //  structure. In case of multiple AndroidOwners, add a fake root
+        waitForIdle()
+
+        return androidOwnerRegistry.getOwners().also {
+            // TODO(b/153632210): This check should be done by callers of collectOwners
+            check(it.isNotEmpty()) {
+                "No compose views found in the app. Is your Activity resumed?"
             }
         }
     }
 
-    override fun unregisterSelfFromEspresso() {
-        super.unregisterSelfFromEspresso()
-        FirstDrawRegistry.setOnDrawnCallback(null)
-    }
-}
-
-internal sealed class BaseIdlingResource : IdlingResource {
-    private val isRegistered = AtomicBoolean(false)
-    private var resourceCallback: IdlingResource.ResourceCallback? = null
-
-    final override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
-        resourceCallback = callback
-    }
-
-    protected fun transitionToIdle() {
-        resourceCallback?.onTransitionToIdle()
-    }
-
-    /**
-     * Registers this resource into Espresso.
-     *
-     * Can be called multiple times.
-     */
-    internal fun registerSelfIntoEspresso() {
-        if (isRegistered.compareAndSet(false, true)) {
-            IdlingRegistry.getInstance().register(this)
-        }
-    }
-
-    /**
-     * Unregisters this resource from Espresso.
-     *
-     * Can be called multiple times.
-     */
-    internal open fun unregisterSelfFromEspresso() {
-        if (isRegistered.compareAndSet(true, false)) {
-            IdlingRegistry.getInstance().unregister(this)
-        }
+    fun getStatementFor(base: Statement): Statement {
+        return androidOwnerRegistry.getStatementFor(base)
     }
 }
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
deleted file mode 100644
index af9d0553a..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.test.junit4.android
-
-import android.os.Handler
-import android.os.Looper
-import android.view.Choreographer
-import androidx.compose.runtime.ExperimentalComposeApi
-import androidx.compose.runtime.Recomposer
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.test.junit4.runOnUiThread
-
-internal class CompositionAwaiter {
-
-    private enum class State {
-        Initialized, Running, Finished, Cancelled
-    }
-
-    private val lock = Any()
-    private var state = State.Initialized
-
-    private val handler = Handler(Looper.getMainLooper())
-    @Suppress("DEPRECATION")
-    private val choreographer = runOnUiThread { Choreographer.getInstance() }
-
-    /**
-     * Starts this awaiter, if it wasn't started, cancelled or finished yet.
-     */
-    fun start() {
-        ifStateIsIn(State.Initialized) {
-            state = State.Running
-            startIdlingResource()
-            handler.post(callback)
-        }
-    }
-
-    /**
-     * Cancels this awaiter if it is running or not yet started. Does nothing if it was already
-     * finished or cancelled.
-     */
-    fun cancel() {
-        ifStateIsIn(State.Initialized, State.Running) {
-            state = State.Cancelled
-            stopIdlingResource()
-            handler.removeCallbacks(callback)
-            choreographer.removeFrameCallback(callback)
-        }
-    }
-
-    private fun startIdlingResource() {
-        ComposeIdlingResource.addCompositionAwaiter()
-    }
-
-    private fun stopIdlingResource() {
-        ComposeIdlingResource.removeCompositionAwaiter()
-    }
-
-    /**
-     * Runs the given [block] if the current [state] is the [validStates]. Synchronizes on
-     * [lock] to make it thread-safe.
-     */
-    private inline fun ifStateIsIn(vararg validStates: State, block: () -> Unit) {
-        try {
-            synchronized(lock) {
-                if (state in validStates) {
-                    block()
-                }
-            }
-        } catch (t: Throwable) {
-            cancel()
-            throw t
-        }
-    }
-
-    @OptIn(ExperimentalComposeApi::class)
-    private fun isIdle(): Boolean {
-        return !Snapshot.current.hasPendingChanges() && !Recomposer.current().hasInvalidations()
-    }
-
-    private val callback = object : Runnable, Choreographer.FrameCallback {
-        override fun run() {
-            ifStateIsIn(State.Running) {
-                if (!isIdle()) {
-                    // not idle, restart check. this makes sure our frame callback
-                    // will be executed _after_ potentially scheduled onCommits
-                    handler.postDelayed(this, 10)
-                } else {
-                    // Is idle. Either nothing is scheduled on the choreographer, in which
-                    // case our callback will be the only one, or something is scheduled on
-                    // the choreographer, in which case our callback will be after it
-                    choreographer.postFrameCallback(this)
-                }
-            }
-        }
-
-        override fun doFrame(frameTime: Long) {
-            ifStateIsIn(State.Running) {
-                if (!isIdle()) {
-                    // not idle, restart check. onCommits have triggered a recomposition
-                    handler.postDelayed(this, 10)
-                } else {
-                    // is idle. onCommits have _not_ triggered a
-                    // recomposition, or there were no onCommits
-                    state = State.Finished
-                    stopIdlingResource()
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/EspressoLink.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/EspressoLink.kt
new file mode 100644
index 0000000..631caf0
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/EspressoLink.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4.android
+
+import androidx.compose.ui.test.junit4.IdlingResourceRegistry
+import androidx.test.espresso.AppNotIdleException
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.IdlingResourceTimeoutException
+import org.junit.runners.model.Statement
+
+internal class EspressoLink(private val registry: IdlingResourceRegistry) : IdlingResource {
+
+    override fun getName(): String = "Compose-Espresso link"
+
+    override fun isIdleNow(): Boolean {
+        return registry.isIdleOrEnsurePolling()
+    }
+
+    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
+        registry.setOnIdleCallback {
+            callback?.onTransitionToIdle()
+        }
+    }
+
+    fun getDiagnosticMessageIfBusy(): String? = registry.getDiagnosticMessageIfBusy()
+
+    fun getStatementFor(base: Statement): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                try {
+                    IdlingRegistry.getInstance().register(this@EspressoLink)
+                    base.evaluate()
+                } finally {
+                    IdlingRegistry.getInstance().unregister(this@EspressoLink)
+                }
+            }
+        }
+    }
+}
+
+// TODO(b/168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
+//  and blocking await Espresso.onIdle().
+internal fun runEspressoOnIdle() {
+    try {
+        Espresso.onIdle()
+    } catch (e: Throwable) {
+
+        // Happens on the global time out, usually when global idling time out is less
+        // or equal to dynamic idling time out or when the timeout is not due to individual
+        // idling resource. This does not necessary mean that it can't be due to idling
+        // resource being busy. So we try to check if it failed due to compose being busy and
+        // add some extra information to the developer.
+        val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
+        if (appNotIdleMaybe != null) {
+            rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
+        }
+
+        // Happens on idling resource taking too long. Espresso gives out which resources caused
+        // it but it won't allow us to give any extra information. So we check if it was our
+        // resource and give more info if we can.
+        val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
+        if (resourceNotIdleMaybe != null) {
+            rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
+        }
+
+        // No match, rethrow
+        throw e
+    }
+}
+
+private fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
+    var diagnosticInfo = ""
+    val listOfIdlingResources = mutableListOf<String>()
+    IdlingRegistry.getInstance().resources.forEach { resource ->
+        if (resource is EspressoLink) {
+            val message = resource.getDiagnosticMessageIfBusy()
+            if (message != null) {
+                diagnosticInfo += "$message \n"
+            }
+        }
+        listOfIdlingResources.add(resource.name)
+    }
+    if (diagnosticInfo.isNotEmpty()) {
+        val prefix = if (wasGlobalTimeout) {
+            "Global time out"
+        } else {
+            "Idling resource timed out"
+        }
+        throw ComposeNotIdleException(
+            "$prefix: possibly due to compose being busy.\n" +
+                diagnosticInfo +
+                "All registered idling resources: " +
+                listOfIdlingResources.joinToString(", "),
+            e
+        )
+    }
+    // No extra info, re-throw the original exception
+    throw e
+}
+
+/**
+ * Tries to find if the given exception or any of its cause is of the type of the provided
+ * throwable T. Returns null if there is no match. This is required as some exceptions end up
+ * wrapped in Runtime or Concurrent exceptions.
+ */
+private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
+    var causeToCheck: Throwable? = e
+    while (causeToCheck != null) {
+        if (causeToCheck is T) {
+            return causeToCheck
+        }
+        causeToCheck = causeToCheck.cause
+    }
+    return null
+}
+
+/**
+ * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
+ */
+class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt
deleted file mode 100644
index 4a81e55..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.test.junit4.android
-
-import android.view.ViewTreeObserver
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.lifecycle.Lifecycle
-import java.util.Collections
-import java.util.WeakHashMap
-
-internal object FirstDrawRegistry {
-    private val notYetDrawn = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
-    private var onDrawnCallback: (() -> Unit)? = null
-
-    private val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-        override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
-            if (registered) {
-                notYetDrawn.add(owner)
-                owner.view.viewTreeObserver.addOnDrawListener(FirstDrawListener(owner))
-            } else {
-                notYetDrawn.remove(owner)
-                dispatchOnDrawn()
-            }
-        }
-    }
-
-    /**
-     * Sets up this registry to listen to the [AndroidOwnerRegistry]. Call this method before
-     * [AndroidOwner]s are registered into that registry, which is before the test activity is
-     * created, or simply right after the [AndroidOwnerRegistry] is setup.
-     */
-    internal fun setupRegistry() {
-        AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
-    }
-
-    /**
-     * Cleans up the changes made by [setupRegistry]. Call this after your test has run.
-     */
-    internal fun tearDownRegistry() {
-        AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-    }
-
-    /**
-     * Returns if all registered owners have finished at least one draw call.
-     */
-    fun haveAllDrawn(): Boolean {
-        return notYetDrawn.all {
-            val viewTreeOwners = it.viewTreeOwners
-            viewTreeOwners == null ||
-                viewTreeOwners.lifecycleOwner.lifecycle.currentState != Lifecycle.State.RESUMED
-        }
-    }
-
-    /**
-     * Adds a [callback] to be called when all registered [AndroidOwner]s have drawn at least
-     * once. The callback will be removed after it is called.
-     */
-    fun setOnDrawnCallback(callback: (() -> Unit)?) {
-        onDrawnCallback = callback
-    }
-
-    /**
-     * Should be called when a registered owner has drawn for the first time. Can be called after
-     * subsequent draws as well, but that is not required.
-     */
-    private fun notifyOwnerDrawn(owner: AndroidOwner) {
-        notYetDrawn.remove(owner)
-        dispatchOnDrawn()
-    }
-
-    private fun dispatchOnDrawn() {
-        if (haveAllDrawn()) {
-            onDrawnCallback?.invoke()
-            onDrawnCallback = null
-        }
-    }
-
-    private class FirstDrawListener(private val owner: AndroidOwner) :
-        ViewTreeObserver.OnDrawListener {
-        private var invoked = false
-
-        override fun onDraw() {
-            if (!invoked) {
-                invoked = true
-                owner.view.post {
-                    // The view was drawn
-                    notifyOwnerDrawn(owner)
-                    val viewTreeObserver = owner.view.viewTreeObserver
-                    if (viewTreeObserver.isAlive) {
-                        viewTreeObserver.removeOnDrawListener(this)
-                    }
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt
deleted file mode 100644
index 959aa19..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.test.junit4.android
-
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.compose.ui.test.ExperimentalTesting
-import androidx.compose.ui.test.InternalTestingApi
-import androidx.compose.ui.test.junit4.isOnUiThread
-import androidx.test.espresso.AppNotIdleException
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.IdlingRegistry
-import androidx.test.espresso.IdlingResourceTimeoutException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.coroutines.resume
-import kotlin.time.ExperimentalTime
-
-@OptIn(InternalTestingApi::class)
-internal class IdleAwaiter {
-
-    fun waitForIdle() {
-        check(!isOnUiThread()) {
-            "Functions that involve synchronization (Assertions, Actions, Synchronization; " +
-                "e.g. assertIsSelected(), doClick(), runOnIdle()) cannot be run " +
-                "from the main thread. Did you nest such a function inside " +
-                "runOnIdle {}, runOnUiThread {} or setContent {}?"
-        }
-
-        // First wait until we have an AndroidOwner (in case an Activity is being started)
-        waitForAndroidOwners()
-        // Then await composition(s)
-        runEspressoOnIdle()
-
-        // TODO(b/155774664): waitForAndroidOwners() may be satisfied by an AndroidOwner from an
-        //  Activity that is about to be paused, in cases where a new Activity is being started.
-        //  That means that AndroidOwnerRegistry.getOwners() may still return an empty list
-        //  between now and when the new Activity has created its AndroidOwner, even though
-        //  waitForAndroidOwners() suggests that we are now guaranteed one.
-    }
-
-    @ExperimentalTesting
-    suspend fun awaitIdle() {
-        // TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
-        //  runEspressoOnIdle() and replace it with a suspend fun that loops while the
-        //  snapshot or the recomposer has pending changes, clocks are busy or owners have
-        //  pending measures or layouts; and do the await on AndroidUiDispatcher.Main
-        // We use Espresso to wait for composition, measure, layout and draw,
-        // and Espresso needs to be called from a non-ui thread; so use Dispatchers.IO
-        withContext(Dispatchers.IO) {
-            // First wait until we have an AndroidOwner (in case an Activity is being started)
-            awaitAndroidOwners()
-            // Then await composition(s)
-            runEspressoOnIdle()
-        }
-    }
-
-    // TODO(168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
-    //  and blocking await Espresso.onIdle().
-    private fun runEspressoOnIdle() {
-        fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
-            var diagnosticInfo = ""
-            val listOfIdlingResources = mutableListOf<String>()
-            IdlingRegistry.getInstance().resources.forEach { resource ->
-                if (resource is IdlingResourceWithDiagnostics) {
-                    val message = resource.getDiagnosticMessageIfBusy()
-                    if (message != null) {
-                        diagnosticInfo += "$message \n"
-                    }
-                }
-                listOfIdlingResources.add(resource.name)
-            }
-            if (diagnosticInfo.isNotEmpty()) {
-                val prefix = if (wasGlobalTimeout) {
-                    "Global time out"
-                } else {
-                    "Idling resource timed out"
-                }
-                throw ComposeNotIdleException(
-                    "$prefix: possibly due to compose being busy.\n" +
-                        diagnosticInfo +
-                        "All registered idling resources: " +
-                        listOfIdlingResources.joinToString(", "),
-                    e
-                )
-            }
-            // No extra info, re-throw the original exception
-            throw e
-        }
-
-        val compositionAwaiter = CompositionAwaiter()
-        try {
-            compositionAwaiter.start()
-            Espresso.onIdle()
-        } catch (e: Throwable) {
-            compositionAwaiter.cancel()
-
-            // Happens on the global time out, usually when global idling time out is less
-            // or equal to dynamic idling time out or when the timeout is not due to individual
-            // idling resource. This does not necessary mean that it can't be due to idling
-            // resource being busy. So we try to check if it failed due to compose being busy and
-            // add some extra information to the developer.
-            val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
-            if (appNotIdleMaybe != null) {
-                rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
-            }
-
-            // Happens on idling resource taking too long. Espresso gives out which resources caused
-            // it but it won't allow us to give any extra information. So we check if it was our
-            // resource and give more info if we can.
-            val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
-            if (resourceNotIdleMaybe != null) {
-                rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
-            }
-
-            // No match, rethrow
-            throw e
-        }
-    }
-
-    /**
-     * Tries to find if the given exception or any of its cause is of the type of the provided
-     * throwable T. Returns null if there is no match. This is required as some exceptions end up
-     * wrapped in Runtime or Concurrent exceptions.
-     */
-    private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
-        var causeToCheck: Throwable? = e
-        while (causeToCheck != null) {
-            if (causeToCheck is T) {
-                return causeToCheck
-            }
-            causeToCheck = causeToCheck.cause
-        }
-        return null
-    }
-
-    private fun ensureAndroidOwnerRegistryIsSetUp() {
-        check(AndroidOwnerRegistry.isSetUp) {
-            "Test not setup properly. Use a ComposeTestRule in your test to be able to interact " +
-                "with composables"
-        }
-    }
-
-    private fun waitForAndroidOwners() {
-        ensureAndroidOwnerRegistryIsSetUp()
-
-        fun hasAndroidOwners(): Boolean = AndroidOwnerRegistry.getOwners().isNotEmpty()
-
-        if (!hasAndroidOwners()) {
-            val latch = CountDownLatch(1)
-            val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-                override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
-                    if (hasAndroidOwners()) {
-                        latch.countDown()
-                    }
-                }
-            }
-            try {
-                AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
-                if (!hasAndroidOwners()) {
-                    latch.await(2, TimeUnit.SECONDS)
-                }
-            } finally {
-                AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-            }
-        }
-    }
-
-    @ExperimentalTesting
-    @OptIn(ExperimentalTime::class)
-    private suspend fun awaitAndroidOwners() {
-        ensureAndroidOwnerRegistryIsSetUp()
-
-        fun hasAndroidOwners(): Boolean = AndroidOwnerRegistry.getOwners().isNotEmpty()
-
-        if (!hasAndroidOwners()) {
-            suspendCancellableCoroutine<Unit> { continuation ->
-                // Make sure we only resume once
-                val didResume = AtomicBoolean(false)
-                fun resume(listener: AndroidOwnerRegistry.OnRegistrationChangedListener) {
-                    if (didResume.compareAndSet(false, true)) {
-                        AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-                        continuation.resume(Unit)
-                    }
-                }
-
-                // Usually we resume if an AndroidOwner is registered while the listener is added
-                val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-                    override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
-                        if (hasAndroidOwners()) {
-                            resume(this)
-                        }
-                    }
-                }
-
-                AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
-                continuation.invokeOnCancellation {
-                    AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-                }
-
-                // Sometimes the AndroidOwner was registered before we added
-                // the listener, in which case we missed our signal
-                if (hasAndroidOwners()) {
-                    resume(listener)
-                }
-            }
-        }
-    }
-}
-
-/**
- * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
- */
-class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
index bacfaf2b..20c2fb6 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
@@ -89,6 +89,13 @@
     }
 }
 
+@Deprecated(
+    message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+        "it from your ComposeTestRule instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
 @ExperimentalTesting
+@Suppress("DocumentExceptions")
 actual fun createAnimationClockRule(): AnimationClockTestRule =
-    DesktopAnimationClockTestRule()
\ No newline at end of file
+    throw UnsupportedOperationException()
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
index 37b9c58..b694cc4 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
@@ -26,13 +26,13 @@
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.InternalTestingApi
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
 import androidx.compose.ui.test.TestOwner
 import androidx.compose.ui.test.createTestContext
-import androidx.compose.ui.test.initCompose
 import androidx.compose.ui.text.input.EditOperation
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.Density
@@ -53,10 +53,6 @@
 class DesktopComposeTestRule : ComposeTestRule {
 
     companion object {
-        init {
-            initCompose()
-        }
-
         var current: DesktopComposeTestRule? = null
     }
 
@@ -135,6 +131,14 @@
         return action().also { waitForIdle() }
     }
 
+    override fun registerIdlingResource(idlingResource: IdlingResource) {
+        // TODO: implement
+    }
+
+    override fun unregisterIdlingResource(idlingResource: IdlingResource) {
+        // TODO: implement
+    }
+
     override fun setContent(composable: @Composable () -> Unit) {
         check(owner == null) {
             "Cannot call setContent twice per test!"
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
index be5d66c..5a02b9c 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
@@ -15,7 +15,6 @@
  */
 package androidx.compose.ui.test.junit4
 
-import androidx.compose.ui.test.initCompose
 import org.jetbrains.skija.Surface
 import org.junit.rules.TestRule
 import org.junit.runner.Description
@@ -168,12 +167,6 @@
     private lateinit var testIdentifier: String
     private lateinit var album: SkijaTestAlbum
 
-    companion object {
-        init {
-            initCompose()
-        }
-    }
-
     val executionQueue = LinkedList<() -> Unit>()
 
     override fun apply(base: Statement, description: Description?): Statement {
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
index 08c2c7c..3fbde5c 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
@@ -48,5 +48,11 @@
     fun advanceClock(milliseconds: Long) = clock.advanceClock(milliseconds)
 }
 
+@Deprecated(
+    message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+        "it from your ComposeTestRule instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
 @ExperimentalTesting
-expect fun createAnimationClockRule(): AnimationClockTestRule
\ No newline at end of file
+expect fun createAnimationClockRule(): AnimationClockTestRule
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
index 9fb0445..1190b77 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
@@ -82,6 +83,16 @@
     suspend fun awaitIdle()
 
     /**
+     * Registers an [IdlingResource] in this test.
+     */
+    fun registerIdlingResource(idlingResource: IdlingResource)
+
+    /**
+     * Unregisters an [IdlingResource] from this test.
+     */
+    fun unregisterIdlingResource(idlingResource: IdlingResource)
+
+    /**
      * Sets the given composable as a content of the current screen.
      *
      * Use this in your tests to setup the UI content to be tested. This should be called exactly
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
new file mode 100644
index 0000000..5afcb3c
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4
+
+import androidx.annotation.VisibleForTesting
+import androidx.compose.ui.test.IdlingResource
+import androidx.compose.ui.test.InternalTestingApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.junit.runners.model.Statement
+
+internal class IdlingResourceRegistry
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+@InternalTestingApi
+internal constructor(
+    private val pollScopeOverride: CoroutineScope?
+) : IdlingResource {
+    // Publicly facing constructor, that doesn't override the poll scope
+    @OptIn(InternalTestingApi::class)
+    constructor() : this(null)
+
+    private val lock = Any()
+
+    // All registered IdlingResources, both idle and busy ones
+    private val idlingResources = mutableSetOf<IdlingResource>()
+    // Each busy resource is mapped to the job that polls it
+    private val busyResources = mutableSetOf<IdlingResource>()
+    // The job that polls the resources until they are idle
+    private var pollJob: Job = Job().also { it.complete() }
+    // The scope in which to launch the poll job, or await the poll job
+    private val pollScope = pollScopeOverride ?: CoroutineScope(Dispatchers.Main)
+
+    private val isPolling: Boolean
+        get() = !pollJob.isCompleted
+
+    // Callback to be called every time when the last busy resource becomes idle
+    private var onIdle: (() -> Unit)? = null
+
+    /**
+     * Returns if all resources are idle
+     */
+    override val isIdleNow: Boolean get() {
+        @Suppress("DEPRECATION_ERROR")
+        return synchronized(lock) {
+            // If a poll job is running, we're not idle now. Let the job do its job.
+            !isPolling && areAllResourcesIdle()
+        }
+    }
+
+    /**
+     * Installs a callback that will be called when the registry transitions from busy to idle.
+     * Intended for the owner of the registry (e.g. AndroidComposeTestRule).
+     */
+    internal fun setOnIdleCallback(callback: () -> Unit) {
+        onIdle = callback
+    }
+
+    /**
+     * Registers the [idlingResource] into the registry
+     */
+    fun registerIdlingResource(idlingResource: IdlingResource) {
+        @Suppress("DEPRECATION_ERROR")
+        synchronized(lock) {
+            idlingResources.add(idlingResource)
+        }
+    }
+
+    /**
+     * Unregisters the [idlingResource] from the registry
+     */
+    fun unregisterIdlingResource(idlingResource: IdlingResource) {
+        @Suppress("DEPRECATION_ERROR")
+        synchronized(lock) {
+            idlingResources.remove(idlingResource)
+            busyResources.remove(idlingResource)
+        }
+    }
+
+    /**
+     * Starts polling the resources until all resources are idle. Won't start polling if all
+     * resources are already idle when this method is invoked, or if polling has already been
+     * started.
+     */
+    internal fun isIdleOrEnsurePolling(): Boolean {
+        @Suppress("DEPRECATION_ERROR")
+        return synchronized(lock) {
+            !isPolling && areAllResourcesIdle().also { isIdle ->
+                if (!isIdle) {
+                    pollJob = pollScope.launch {
+                        do {
+                            delay(20)
+                        } while (!areAllResourcesIdle())
+                        onIdle?.invoke()
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks all resources for idleness, updates [busyResources] and returns if the registry is
+     * idle now.
+     */
+    private fun areAllResourcesIdle(): Boolean {
+        @Suppress("DEPRECATION_ERROR")
+        return synchronized(lock) {
+            busyResources.clear()
+            idlingResources.filterTo(busyResources) { !it.isIdleNow }.isEmpty()
+        }
+    }
+
+    override fun getDiagnosticMessageIfBusy(): String? {
+        val (idle, busy) =
+            @Suppress("DEPRECATION_ERROR")
+            synchronized(lock) {
+                if (busyResources.isEmpty()) {
+                    return null
+                }
+                Pair(
+                    (idlingResources - busyResources).toList(),
+                    busyResources.map { it.getDiagnosticMessageIfBusy() ?: it.toString() }
+                )
+            }
+        return "IdlingResourceRegistry has the following idling resources registered:" +
+            busy.map { "\n- [busy] ${it.indentBy("         ")}" } +
+            idle.map { "\n- [idle] $it" } +
+            if (idle.isEmpty() && busy.isEmpty()) "\n<none>" else ""
+    }
+
+    /**
+     * Adds the given [prefix] after all non-terminal new lines.
+     *
+     * For example: `"\nfoo\nbar\n".indentBy("-")` gives `"\n-foo\n-bar\n"`
+     */
+    private fun String.indentBy(prefix: String): String {
+        return replace("\n(?=.)".toRegex(), "\n$prefix")
+    }
+
+    fun getStatementFor(base: Statement): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                try {
+                    base.evaluate()
+                } finally {
+                    if (pollScopeOverride == null) {
+                        if (pollScope.coroutineContext[Job] != null) pollScope.cancel()
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 8144343..6a6fa69 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -86,9 +86,9 @@
     method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
     method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
     method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
+    method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean ignoreCase);
     method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
     method @Deprecated public static androidx.compose.ui.test.SemanticsMatcher hasInputMethodsSupport();
-    method public static androidx.compose.ui.test.SemanticsMatcher hasLabel(String label, optional boolean ignoreCase);
     method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
     method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
     method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
@@ -120,11 +120,11 @@
   }
 
   public final class FindersKt {
-    method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+    method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
-    method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+    method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
@@ -179,6 +179,12 @@
     method public static void up(androidx.compose.ui.test.GestureScope, optional int pointerId);
   }
 
+  public interface IdlingResource {
+    method public default String? getDiagnosticMessageIfBusy();
+    method public boolean isIdleNow();
+    property public abstract boolean isIdleNow;
+  }
+
   @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
   }
 
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index 8144343..6a6fa69 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -86,9 +86,9 @@
     method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
     method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
     method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
+    method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean ignoreCase);
     method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
     method @Deprecated public static androidx.compose.ui.test.SemanticsMatcher hasInputMethodsSupport();
-    method public static androidx.compose.ui.test.SemanticsMatcher hasLabel(String label, optional boolean ignoreCase);
     method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
     method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
     method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
@@ -120,11 +120,11 @@
   }
 
   public final class FindersKt {
-    method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+    method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
-    method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+    method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
@@ -179,6 +179,12 @@
     method public static void up(androidx.compose.ui.test.GestureScope, optional int pointerId);
   }
 
+  public interface IdlingResource {
+    method public default String? getDiagnosticMessageIfBusy();
+    method public boolean isIdleNow();
+    property public abstract boolean isIdleNow;
+  }
+
   @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
   }
 
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 8144343..6a6fa69 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -86,9 +86,9 @@
     method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
     method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
     method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
+    method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean ignoreCase);
     method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
     method @Deprecated public static androidx.compose.ui.test.SemanticsMatcher hasInputMethodsSupport();
-    method public static androidx.compose.ui.test.SemanticsMatcher hasLabel(String label, optional boolean ignoreCase);
     method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
     method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
     method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
@@ -120,11 +120,11 @@
   }
 
   public final class FindersKt {
-    method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+    method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
-    method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+    method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
     method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
@@ -179,6 +179,12 @@
     method public static void up(androidx.compose.ui.test.GestureScope, optional int pointerId);
   }
 
+  public interface IdlingResource {
+    method public default String? getDiagnosticMessageIfBusy();
+    method public boolean isIdleNow();
+    property public abstract boolean isIdleNow;
+  }
+
   @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
   }
 
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 7a19f17..3e55c2a 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -128,7 +128,6 @@
 android {
     tasks.withType(KotlinCompile).configureEach {
         kotlinOptions {
-            freeCompilerArgs += ["-XXLanguage:-NewInference"]
             useIR = true
         }
     }
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
index 29ee38a..9319748 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
@@ -24,7 +24,7 @@
 import androidx.compose.ui.semantics.AccessibilityAction
 import androidx.compose.ui.semantics.SemanticsPropertyKey
 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.accessibilityLabel
+import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.filters.MediumTest
@@ -46,18 +46,18 @@
             val state = remember { mutableStateOf("Nothing") }
             BoundaryNode {
                 setString("SetString") { state.value = it; return@setString true }
-                accessibilityLabel = state.value
+                contentDescription = state.value
             }
         }
 
-        rule.onNodeWithLabel("Nothing")
+        rule.onNodeWithContentDescription("Nothing")
             .assertExists()
             .performSemanticsAction(MyActions.SetString) { it("Hello") }
 
-        rule.onNodeWithLabel("Nothing")
+        rule.onNodeWithContentDescription("Nothing")
             .assertDoesNotExist()
 
-        rule.onNodeWithLabel("Hello")
+        rule.onNodeWithContentDescription("Hello")
             .assertExists()
     }
 
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
index 0a59655..47c42c1 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
@@ -19,28 +19,25 @@
 import androidx.test.espresso.matcher.ViewMatchers
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.findClosestParentNode
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.semantics.SemanticsNode
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal actual fun SemanticsNodeInteraction.checkIsDisplayed(): Boolean {
     // hierarchy check - check layout nodes are visible
     val errorMessageOnFail = "Failed to perform isDisplayed check."
     val node = fetchSemanticsNode(errorMessageOnFail)
 
-    fun isNotPlaced(node: LayoutNode): Boolean {
+    fun isNotPlaced(node: LayoutInfo): Boolean {
         return !node.isPlaced
     }
 
-    val layoutNode = node.componentNode
-    if (isNotPlaced(layoutNode) || layoutNode.findClosestParentNode(::isNotPlaced) != null) {
+    val layoutInfo = node.layoutInfo
+    if (isNotPlaced(layoutInfo) || layoutInfo.findClosestParentNode(::isNotPlaced) != null) {
         return false
     }
 
-    (layoutNode.owner as? AndroidOwner)?.let {
+    (node.owner as? ViewRootForTest)?.let {
         if (!ViewMatchers.isDisplayed().matches(it.view)) {
             return false
         }
@@ -55,9 +52,8 @@
     return (globalRect.width > 0f && globalRect.height > 0f)
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal actual fun SemanticsNode.clippedNodeBoundsInWindow(): Rect {
-    val composeView = (componentNode.owner as AndroidOwner).view
+    val composeView = (owner as ViewRootForTest).view
     val rootLocationInWindow = intArrayOf(0, 0).let {
         composeView.getLocationInWindow(it)
         Offset(it[0].toFloat(), it[1].toFloat())
@@ -65,9 +61,8 @@
     return boundsInRoot.translate(rootLocationInWindow)
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal actual fun SemanticsNode.isInScreenBounds(): Boolean {
-    val composeView = (componentNode.owner as AndroidOwner).view
+    val composeView = (owner as ViewRootForTest).view
 
     // Window relative bounds of our node
     val nodeBoundsInWindow = clippedNodeBoundsInWindow()
@@ -85,4 +80,24 @@
         nodeBoundsInWindow.left >= globalRootRect.left &&
         nodeBoundsInWindow.right <= globalRootRect.right &&
         nodeBoundsInWindow.bottom <= globalRootRect.bottom
-}
\ No newline at end of file
+}
+
+/**
+ * Executes [selector] on every parent of this [LayoutInfo] and returns the closest
+ * [LayoutInfo] to return `true` from [selector] or null if [selector] returns false
+ * for all ancestors.
+ */
+private fun LayoutInfo.findClosestParentNode(
+    selector: (LayoutInfo) -> Boolean
+): LayoutInfo? {
+    var currentParent = this.parentInfo
+    while (currentParent != null) {
+        if (selector(currentParent)) {
+            return currentParent
+        } else {
+            currentParent = currentParent.parentInfo
+        }
+    }
+
+    return null
+}
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
index 2e02517..e9de46c 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
@@ -21,8 +21,7 @@
 import android.view.Window
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.test.android.captureRegionToImage
 import androidx.compose.ui.window.DialogWindowProvider
@@ -54,8 +53,7 @@
         )
     }
 
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val view = (node.componentNode.owner as AndroidOwner).view
+    val view = (node.owner as ViewRootForTest).view
 
     // If we are in dialog use its window to capture the bitmap
     val dialogParentNodeMaybe = node.findClosestParentNode(includeSelf = true) {
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
index a708660..6dfc89b 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
@@ -28,15 +28,15 @@
 import androidx.compose.runtime.dispatch.AndroidUiDispatcher
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import kotlin.math.max
 
-internal actual fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
-    require(owner is AndroidOwner) {
-        "InputDispatcher currently only supports dispatching to AndroidOwner, not to " +
+internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
+    require(owner is ViewRootForTest) {
+        "InputDispatcher currently only supports dispatching to ViewRootForTest, not to " +
             owner::class.java.simpleName
     }
     val view = owner.view
@@ -45,9 +45,9 @@
 
 internal class AndroidInputDispatcher(
     testContext: TestContext,
-    owner: AndroidOwner?,
+    owner: Owner?,
     private val sendEvent: (MotionEvent) -> Unit
-) : PersistingInputDispatcher(testContext, owner) {
+) : InputDispatcher(testContext, owner) {
 
     private val batchLock = Any()
     // Batched events are generated just-in-time, given the "lateness" of the dispatching (see
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
index b4ae8f5..c680bb8 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
@@ -20,7 +20,6 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.boundsInParent
 import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.semantics.AccessibilityAction
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsPropertyKey
@@ -65,10 +64,8 @@
     // Figure out the (clipped) bounds of the viewPort in its direct parent's content area, in
     // root coordinates. We only want the clipping from the direct parent on the scrollable, not
     // from any other ancestors.
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val viewPortInParent = scrollableNode.componentNode.coordinates.boundsInParent
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val parentInRoot = scrollableNode.componentNode.coordinates.parentCoordinates
+    val viewPortInParent = scrollableNode.layoutInfo.coordinates.boundsInParent
+    val parentInRoot = scrollableNode.layoutInfo.coordinates.parentCoordinates
         ?.positionInRoot ?: Offset.Zero
 
     val viewPort = viewPortInParent.translate(parentInRoot)
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt
index d9ba5c5..9e06eed 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt
@@ -167,11 +167,11 @@
 
 /**
  * Asserts the node's label equals the given String.
- * For further details please check [SemanticsProperties.AccessibilityLabel].
+ * For further details please check [SemanticsProperties.ContentDescription].
  * Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
  */
 fun SemanticsNodeInteraction.assertLabelEquals(value: String): SemanticsNodeInteraction =
-    assert(hasLabel(value))
+    assert(hasContentDescription(value))
 
 /**
  * Asserts the node's text equals the given String.
@@ -184,7 +184,7 @@
 /**
  * Asserts the node's value equals the given value.
  *
- * For further details please check [SemanticsProperties.AccessibilityValue].
+ * For further details please check [SemanticsProperties.StateDescription].
  * Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
  */
 fun SemanticsNodeInteraction.assertValueEquals(value: String): SemanticsNodeInteraction =
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
index 8626416..ac7ff43 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
@@ -161,13 +161,13 @@
  * @param label Text to match.
  * @param ignoreCase Whether case should be ignored.
  *
- * @see SemanticsProperties.AccessibilityLabel
+ * @see SemanticsProperties.ContentDescription
  */
-fun hasLabel(label: String, ignoreCase: Boolean = false): SemanticsMatcher {
+fun hasContentDescription(label: String, ignoreCase: Boolean = false): SemanticsMatcher {
     return SemanticsMatcher(
-        "${SemanticsProperties.AccessibilityLabel.name} = '$label' (ignoreCase: $ignoreCase)"
+        "${SemanticsProperties.ContentDescription.name} = '$label' (ignoreCase: $ignoreCase)"
     ) {
-        it.config.getOrNull(SemanticsProperties.AccessibilityLabel).equals(label, ignoreCase)
+        it.config.getOrNull(SemanticsProperties.ContentDescription).equals(label, ignoreCase)
     }
 }
 
@@ -212,10 +212,10 @@
  *
  * @param value Value to match.
  *
- * @see SemanticsProperties.AccessibilityValue
+ * @see SemanticsProperties.StateDescription
  */
 fun hasValue(value: String): SemanticsMatcher = SemanticsMatcher.expectValue(
-    SemanticsProperties.AccessibilityValue, value
+    SemanticsProperties.StateDescription, value
 )
 
 /**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt
index 6d67243..2489cb4 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt
@@ -45,7 +45,7 @@
 ): SemanticsNodeInteractionCollection = onAllNodes(hasTestTag(testTag), useUnmergedTree)
 
 /**
- * Finds a semantics node with the given label as its accessibilityLabel.
+ * Finds a semantics node with the given contentDescription.
  *
  * For usage patterns and semantics concepts see [SemanticsNodeInteraction]
  *
@@ -53,11 +53,11 @@
  *
  * @see SemanticsNodeInteractionsProvider.onNode for general find method.
  */
-fun SemanticsNodeInteractionsProvider.onNodeWithLabel(
+fun SemanticsNodeInteractionsProvider.onNodeWithContentDescription(
     label: String,
     ignoreCase: Boolean = false,
     useUnmergedTree: Boolean = false
-): SemanticsNodeInteraction = onNode(hasLabel(label, ignoreCase), useUnmergedTree)
+): SemanticsNodeInteraction = onNode(hasContentDescription(label, ignoreCase), useUnmergedTree)
 
 /**
  * Finds a semantincs node with the given text.
@@ -105,18 +105,19 @@
 ): SemanticsNodeInteractionCollection = onAllNodes(hasText(text, ignoreCase), useUnmergedTree)
 
 /**
- * Finds all semantics nodes with the given label as AccessibilityLabel.
+ * Finds all semantics nodes with the given label as ContentDescription.
  *
  * For usage patterns and semantics concepts see [SemanticsNodeInteraction]
  *
  * @param useUnmergedTree Find within merged composables like Buttons.
  * @see SemanticsNodeInteractionsProvider.onAllNodes for general find method.
  */
-fun SemanticsNodeInteractionsProvider.onAllNodesWithLabel(
+fun SemanticsNodeInteractionsProvider.onAllNodesWithContentDescription(
     label: String,
     ignoreCase: Boolean = false,
     useUnmergedTree: Boolean = false
-): SemanticsNodeInteractionCollection = onAllNodes(hasLabel(label, ignoreCase), useUnmergedTree)
+): SemanticsNodeInteractionCollection =
+    onAllNodes(hasContentDescription(label, ignoreCase), useUnmergedTree)
 
 /**
  * Finds all semantics nodes with text that contains the given substring.
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
index 9f7e5e4..c745d64 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.gesture.DoubleTapTimeout
 import androidx.compose.ui.gesture.LongPressTimeout
 import androidx.compose.ui.layout.globalBounds
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.unit.Duration
 import androidx.compose.ui.unit.IntSize
@@ -107,12 +106,11 @@
         }
 
     // Convenience property
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    private val owner get() = semanticsNode.componentNode.owner
+    private val owner get() = semanticsNode.owner
 
     // TODO(b/133217292): Better error: explain which gesture couldn't be performed
     private var _inputDispatcher: InputDispatcher? =
-        InputDispatcher(testContext, checkNotNull(owner))
+        createInputDispatcher(testContext, checkNotNull(owner))
     internal val inputDispatcher
         get() = checkNotNull(_inputDispatcher) {
             "Can't send gesture, (Partial)GestureScope has already been disposed"
@@ -287,8 +285,7 @@
  * @param position A position in local coordinates
  */
 private fun GestureScope.localToGlobal(position: Offset): Offset {
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    return position + semanticsNode.componentNode.coordinates.globalBounds.topLeft
+    return position + semanticsNode.layoutInfo.coordinates.globalBounds.topLeft
 }
 
 /**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/IdlingResource.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/IdlingResource.kt
new file mode 100644
index 0000000..7abd443
--- /dev/null
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/IdlingResource.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+/**
+ * Represents a resource of an application under test which can cause asynchronous background
+ * work to happen during test execution (e.g. an http request in response to a button click).
+ *
+ * By default, all interactions from the test with the compose tree (finding nodes, performing
+ * gestures, making assertions) will be synchronized with pending work in Compose's internals
+ * (such as applying state changes, recomposing, measuring, etc). This ensures that the UI is in
+ * a stable state when the interactions are performed, so that e.g. no pending recompositions are
+ * still scheduled that could potentially change the UI. However, any asynchronous work that is
+ * not done through one of Compose's mechanisms won't be included in the default synchronization.
+ * For such work, test authors can create an [IdlingResource] and register it into the test with
+ * [registerIdlingResource][androidx.compose.ui.test.junit4.ComposeTestRule
+ * .registerIdlingResource], and the interaction will wait for that resource to become idle prior
+ * to performing it.
+ */
+interface IdlingResource {
+    /**
+     * Whether or not the [IdlingResource] is idle when reading this value. This should always be
+     * called from the main thread, which is why it should be lightweight and fast.
+     *
+     * If one idling resource returns `false`, the synchronization system will keep polling all
+     * idling resources until they are all idle.
+     */
+    val isIdleNow: Boolean
+
+    /**
+     * Returns diagnostics that explain why the idling resource is busy, or `null` if the
+     * resource is not busy. Default implementation returns `null`.
+     */
+    fun getDiagnosticMessageIfBusy(): String? = null
+}
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 7cbf710..01baa72 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
@@ -24,7 +24,7 @@
 import kotlin.math.max
 import kotlin.math.roundToInt
 
-internal expect fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher
+internal expect fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher
 
 /**
  * Dispatcher to inject full and partial gestures. An [InputDispatcher] is created at the
@@ -37,9 +37,6 @@
  * Clients of [InputDispatcher] should only call methods for the first stage listed below, the
  * second stage is handled by [performGesture].
  *
- * Implementations of [InputDispatcher] must derive from [PersistingInputDispatcher], which
- * handles state restoration.
- *
  * Full gestures:
  * * [enqueueClick]
  * * [enqueueSwipe]
@@ -56,7 +53,10 @@
  * Chaining methods:
  * * [enqueueDelay]
  */
-internal abstract class InputDispatcher {
+internal abstract class InputDispatcher(
+    private val testContext: TestContext,
+    private val owner: Owner?
+) {
     companion object {
         /**
          * Whether or not injection of events should be suspended in between events until [now]
@@ -124,6 +124,26 @@
      */
     protected abstract val now: Long
 
+    init {
+        val state = testContext.states.remove(owner)
+        if (state?.partialGesture != null) {
+            nextDownTime = state.nextDownTime
+            gestureLateness = state.gestureLateness
+            partialGesture = state.partialGesture
+        }
+    }
+
+    protected open fun saveState(owner: Owner?) {
+        if (owner != null) {
+            testContext.states[owner] =
+                InputDispatcherState(
+                    nextDownTime,
+                    gestureLateness,
+                    partialGesture
+                )
+        }
+    }
+
     /**
      * Generates the downTime of the next gesture with the given [duration]. The gesture's
      * [duration] is necessary to facilitate chaining of gestures: if another gesture is made
@@ -549,7 +569,12 @@
     /**
      * Called when this [InputDispatcher] is about to be discarded, from [GestureScope.dispose].
      */
-    abstract fun dispose()
+    fun dispose() {
+        saveState(owner)
+        onDispose()
+    }
+
+    protected open fun onDispose() {}
 }
 
 /**
@@ -565,3 +590,22 @@
     val lastPositions = mutableMapOf(Pair(pointerId, startPosition))
     var hasPointerUpdates: Boolean = false
 }
+
+/**
+ * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored
+ * when the [GestureScope] is recreated.
+ *
+ * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
+ * This property will only be restored if an incomplete gesture was in progress when the
+ * state of the [InputDispatcher] was saved.
+ * @param gestureLateness The time difference in milliseconds between enqueuing the first
+ * event of the gesture and dispatching it. Depending on the implementation of
+ * [InputDispatcher], this may or may not be used.
+ * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
+ * when the state of the [InputDispatcher] was saved, this will be `null`.
+ */
+internal data class InputDispatcherState(
+    val nextDownTime: Long,
+    var gestureLateness: Long?,
+    val partialGesture: PartialGesture?
+)
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
index a3a63f0..86871e9 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
@@ -16,20 +16,16 @@
 
 package androidx.compose.ui.test
 
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 
 /**
  * Send the specified [KeyEvent] to the focused component.
  *
  * @return true if the event was consumed. False otherwise.
  */
-@OptIn(ExperimentalKeyInput::class)
 fun SemanticsNodeInteraction.performKeyPress(keyEvent: KeyEvent): Boolean {
     val semanticsNode = fetchSemanticsNode("Failed to send key Event (${keyEvent.key})")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = semanticsNode.componentNode.owner
+    val owner = semanticsNode.owner
     requireNotNull(owner) { "Failed to find owner" }
     @OptIn(InternalTestingApi::class)
     return testContext.testOwner.runOnUiThread { owner.sendKeyEvent(keyEvent) }
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Output.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Output.kt
index eb42c37..dc16fb4 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Output.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Output.kt
@@ -244,4 +244,10 @@
         append(indent)
         append("MergeDescendants = 'true'")
     }
+
+    if (config.isClearingSemantics) {
+        appendLine()
+        append(indent)
+        append("ReplaceSemantics = 'true'")
+    }
 }
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt
deleted file mode 100644
index 0ef9f06..0000000
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.test
-
-import androidx.compose.ui.node.Owner
-
-internal abstract class PersistingInputDispatcher(
-    private val testContext: TestContext,
-    private val owner: Owner?
-) : InputDispatcher() {
-
-    init {
-        val state = testContext.states.remove(owner)
-        if (state?.partialGesture != null) {
-            nextDownTime = state.nextDownTime
-            gestureLateness = state.gestureLateness
-            partialGesture = state.partialGesture
-        }
-    }
-
-    protected open fun saveState(owner: Owner?) {
-        if (owner != null) {
-            testContext.states[owner] =
-                InputDispatcherState(nextDownTime, gestureLateness, partialGesture)
-        }
-    }
-
-    final override fun dispose() {
-        saveState(owner)
-        onDispose()
-    }
-
-    open fun onDispose() {}
-
-    /**
-     * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored
-     * when the [GestureScope] is recreated.
-     *
-     * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
-     * This property will only be restored if an incomplete gesture was in progress when the
-     * state of the [InputDispatcher] was saved.
-     * @param gestureLateness The time difference in milliseconds between enqueuing the first
-     * event of the gesture and dispatching it. Depending on the implementation of
-     * [InputDispatcher], this may or may not be used.
-     * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
-     * when the state of the [InputDispatcher] was saved, this will be `null`.
-     */
-    internal data class InputDispatcherState(
-        val nextDownTime: Long,
-        var gestureLateness: Long?,
-        val partialGesture: PartialGesture?
-    )
-}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
index edbacd7..74a2c6d 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
@@ -17,7 +17,6 @@
 package androidx.compose.ui.test
 
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.unit.Density
 
@@ -249,8 +248,7 @@
     operation: Density.(SemanticsNode) -> R
 ): R {
     val node = fetchSemanticsNode("Failed to retrieve density for the node.")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val density = node.componentNode.owner!!.density
+    val density = node.owner!!.density
     return operation.invoke(density, node)
 }
 
@@ -258,8 +256,7 @@
     assertion: Density.(Rect) -> Unit
 ): SemanticsNodeInteraction {
     val node = fetchSemanticsNode("Failed to retrieve bounds of the node.")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val density = node.componentNode.owner!!.density
+    val density = node.owner!!.density
 
     assertion.invoke(density, node.unclippedBoundsInRoot)
     return this
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
index c4084d0..9624c69 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
@@ -19,7 +19,6 @@
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.semantics.getAllSemanticsNodes
-import androidx.compose.ui.test.PersistingInputDispatcher.InputDispatcherState
 import androidx.compose.ui.text.input.EditOperation
 import androidx.compose.ui.text.input.ImeAction
 
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
index 94671fd..3d9f038 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
@@ -24,14 +24,14 @@
 import androidx.compose.ui.platform.DesktopOwner
 import androidx.compose.ui.unit.Uptime
 
-internal actual fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
+internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
     return DesktopInputDispatcher(testContext, owner as DesktopOwner)
 }
 
 internal class DesktopInputDispatcher(
     testContext: TestContext,
     val owner: DesktopOwner
-) : PersistingInputDispatcher(testContext, owner) {
+) : InputDispatcher(testContext, owner) {
     companion object {
         var gesturePointerId = 0L
     }
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
index 8720246..3c47455 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
@@ -50,10 +50,4 @@
         owner.draw(canvas)
         return owners
     }
-
-    companion object {
-        init {
-            initCompose()
-        }
-    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -62,8 +62,8 @@
   public final class AnnotatedStringKt {
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
-    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
-    method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
     method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -62,8 +62,8 @@
   public final class AnnotatedStringKt {
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
-    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
-    method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
     method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -62,8 +62,8 @@
   public final class AnnotatedStringKt {
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
-    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
-    method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
     method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
index 4b443f7..5eb4cee 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
@@ -21,7 +21,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ParagraphStyle
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextIndent
@@ -124,7 +124,7 @@
 @Sampled
 fun AnnotatedStringBuilderLambdaSample() {
     // create an AnnotatedString using the lambda builder
-    annotatedString {
+    buildAnnotatedString {
         // append "Hello" with red text color
         withStyle(SpanStyle(color = Color.Red)) {
             append("Hello")
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
index ba55a26..17c7b19 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
@@ -20,7 +20,7 @@
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.BaselineShift
 import androidx.compose.ui.text.withStyle
 import androidx.compose.ui.unit.sp
@@ -30,7 +30,7 @@
 fun BaselineShiftSample() {
     Text(
         fontSize = 20.sp,
-        text = annotatedString {
+        text = buildAnnotatedString {
             append(text = "Hello")
             withStyle(SpanStyle(baselineShift = BaselineShift.Superscript, fontSize = 16.sp)) {
                 append("superscript")
@@ -45,7 +45,7 @@
 @Sampled
 @Composable
 fun BaselineShiftAnnotatedStringSample() {
-    val annotatedString = annotatedString {
+    val annotatedString = buildAnnotatedString {
         append("Text ")
         withStyle(SpanStyle(baselineShift = BaselineShift.Superscript)) {
             append("Demo")
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
index 0fc3863..51f383c 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
@@ -21,7 +21,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.text.ParagraphStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextIndent
 import androidx.compose.ui.unit.sp
@@ -57,7 +57,7 @@
     )
 
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             append(text)
             addStyle(paragraphStyle1, 0, text.indexOf('\n') + 1)
             addStyle(paragraphStyle2, text.indexOf('\n') + 1, text.length)
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
index e85c75f..451b611 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
@@ -21,8 +21,8 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
 import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.sp
 
 @Sampled
@@ -30,7 +30,7 @@
 fun SpanStyleSample() {
     Text(
         fontSize = 16.sp,
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(color = Color.Red)) {
                 append("Hello")
             }
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
index 11456e3..6f01560 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
@@ -1431,7 +1431,7 @@
      * Helper function which creates an AnnotatedString where each input string becomes a paragraph.
      */
     private fun createAnnotatedString(paragraphs: List<String>): AnnotatedString {
-        return annotatedString {
+        return buildAnnotatedString {
             for (paragraph in paragraphs) {
                 pushStyle(ParagraphStyle())
                 append(paragraph)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
index 97ec592..c14cc7a 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
@@ -242,7 +242,7 @@
      * Helper function which creates an AnnotatedString where each input string becomes a paragraph.
      */
     private fun createAnnotatedString(paragraphs: List<String>): AnnotatedString {
-        return annotatedString {
+        return buildAnnotatedString {
             for (paragraph in paragraphs) {
                 pushStyle(ParagraphStyle())
                 append(paragraph)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index a88a311..2417156 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -33,7 +33,7 @@
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TestFontResourceLoader
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
@@ -62,7 +62,7 @@
     @Test
     fun toAccessibilitySpannableString_with_locale() {
         val languageTag = "en-GB"
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(localeList = LocaleList(languageTag))) {
                 append("world")
@@ -84,7 +84,7 @@
     @Test
     fun toAccessibilitySpannableString_with_color() {
         val color = Color.Black
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(color = color)) {
                 append("world")
@@ -105,7 +105,7 @@
     @Test
     fun toAccessibilitySpannableString_with_fontSizeInSp() {
         val fontSize = 12.sp
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontSize = fontSize)) {
                 append("world")
@@ -126,7 +126,7 @@
     @Test
     fun toAccessibilitySpannableString_with_fontSizeInEm() {
         val fontSize = 2.em
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontSize = fontSize)) {
                 append("world")
@@ -146,7 +146,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_fontWeightBold() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
                 append("world")
@@ -166,7 +166,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_italic() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
                 append("world")
@@ -187,7 +187,7 @@
     @Test
     fun toAccessibilitySpannableString_with_fontFamily() {
         val fontFamily = FontFamily.Monospace
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontFamily = fontFamily)) {
                 append("world")
@@ -207,7 +207,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_underline() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
                 append("world")
@@ -225,7 +225,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_lineThrough() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
                 append("world")
@@ -244,7 +244,7 @@
     @Test
     fun toAccessibilitySpannableString_with_scaleX() {
         val scaleX = 1.2f
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(
                 style = SpanStyle(textGeometricTransform = TextGeometricTransform(scaleX = scaleX))
@@ -267,7 +267,7 @@
     @Test
     fun toAccessibilitySpannableString_with_background() {
         val backgroundColor = Color.Red
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(background = backgroundColor)) {
                 append("world")
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 189c1ad..732c765 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
@@ -743,10 +743,10 @@
  * @param builder lambda to modify [AnnotatedString.Builder]
  */
 @Deprecated(
-    message = "Renamed to annotatedString.",
-    replaceWith = ReplaceWith("annotatedString")
+    message = "Renamed to buildAnnotatedString.",
+    replaceWith = ReplaceWith("buildAnnotatedString")
 )
-inline fun AnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
+inline fun annotatedString(builder: (Builder).() -> Unit): AnnotatedString =
     Builder().apply(builder).toAnnotatedString()
 
 /**
@@ -757,7 +757,7 @@
  *
  * @param builder lambda to modify [AnnotatedString.Builder]
  */
-inline fun annotatedString(builder: (Builder).() -> Unit): AnnotatedString =
+inline fun buildAnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
     Builder().apply(builder).toAnnotatedString()
 
 /**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
index 0f1e350..42a8fbe 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
@@ -58,7 +58,7 @@
     }
 
     /**
-     * [ParagraphIntrinsics] for each paragraph included in the [annotatedString]. For empty string
+     * [ParagraphIntrinsics] for each paragraph included in the [buildAnnotatedString]. For empty string
      * there will be a single empty paragraph intrinsics info.
      */
     internal val infoList: List<ParagraphIntrinsicInfo>
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
index ddf38bd3..efac85d 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
@@ -278,11 +278,11 @@
         return null
     }
 
-    private fun getBoxBackwardByOffset(offset: Int): TextBox? {
+    private fun getBoxBackwardByOffset(offset: Int, end: Int = offset): TextBox? {
         var from = offset - 1
         while (from >= 0) {
             val box = para.getRectsForRange(
-                from, offset,
+                from, end,
                 RectHeightMode.STRUT, RectWidthMode.TIGHT
             ).firstOrNull()
             when {
@@ -314,8 +314,10 @@
         return para.getGlyphPositionAtCoordinate(position.x, position.y).position
     }
 
-    override fun getBoundingBox(offset: Int) =
-        getBoxForwardByOffset(offset)!!.rect.toComposeRect()
+    override fun getBoundingBox(offset: Int): Rect {
+        val box = getBoxForwardByOffset(offset) ?: getBoxBackwardByOffset(offset, text.length)!!
+        return box.rect.toComposeRect()
+    }
 
     override fun getWordBoundary(offset: Int) = para.getWordBoundary(offset).let {
         TextRange(it.start, it.end)
@@ -554,6 +556,8 @@
 
             when (op) {
                 is Op.StyleAdd -> {
+                    // cached SkTextStyled could was loaded with a different font loader
+                    ensureFontsAreRegistered(fontLoader, op.style)
                     pb.pushStyle(makeSkTextStyle(op.style))
                 }
                 is Op.PutPlaceholder -> {
@@ -585,6 +589,12 @@
         return pb.build()
     }
 
+    private fun ensureFontsAreRegistered(fontLoader: FontLoader, style: ComputedStyle) {
+        style.fontFamily?.let {
+            fontLoader.ensureRegistered(it)
+        }
+    }
+
     private sealed class Op {
         abstract val position: Int
 
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
index f81951e..641b23cd 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
@@ -491,7 +491,7 @@
         val paragraphStyle1 = ParagraphStyle(textAlign = TextAlign.Right)
         val paragraphStyle2 = ParagraphStyle(textAlign = TextAlign.Right)
 
-        val buildResult = annotatedString {
+        val buildResult = buildAnnotatedString {
             withStyle(paragraphStyle1) {
                 withStyle(spanStyle1) {
                     append(text1)
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
index 909a52e..5a422e9 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
@@ -324,7 +324,7 @@
 
     @Test
     fun subSequence_withAnnotations_noIntersection() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
@@ -338,7 +338,7 @@
 
     @Test
     fun subSequence_withAnnotations_collapsedRange() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
@@ -370,7 +370,7 @@
 
     @Test
     fun subSequence_withAnnotations_hasIntersection() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
@@ -398,7 +398,7 @@
 
     @Test
     fun subSequence_withAnnotations_containsRange() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
diff --git a/compose/ui/ui-tooling/api/current.txt b/compose/ui/ui-tooling/api/current.txt
index 23de9c4..2d94392 100644
--- a/compose/ui/ui-tooling/api/current.txt
+++ b/compose/ui/ui-tooling/api/current.txt
@@ -12,7 +12,7 @@
     method public final java.util.Collection<java.lang.Object> getData();
     method public final Object? getKey();
     method public final androidx.compose.ui.tooling.SourceLocation? getLocation();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public final String? getName();
     method public java.util.List<androidx.compose.ui.tooling.ParameterInformation> getParameters();
     property public final androidx.compose.ui.unit.IntBounds box;
@@ -20,7 +20,7 @@
     property public final java.util.Collection<java.lang.Object> data;
     property public final Object? key;
     property public final androidx.compose.ui.tooling.SourceLocation? location;
-    property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+    property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
     property public final String? name;
     property public java.util.List<androidx.compose.ui.tooling.ParameterInformation> parameters;
   }
@@ -41,9 +41,9 @@
   }
 
   public final class NodeGroup extends androidx.compose.ui.tooling.Group {
-    ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
+    ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
     method public Object getNode();
-    property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+    property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
     property public final Object node;
   }
 
@@ -74,7 +74,7 @@
   }
 
   public final class SlotTreeKt {
-    method public static androidx.compose.ui.tooling.Group asTree(androidx.compose.runtime.SlotTable);
+    method public static androidx.compose.ui.tooling.Group asTree(androidx.compose.runtime.CompositionData);
     method public static String? getPosition(androidx.compose.ui.tooling.Group);
   }
 
diff --git a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
index 23de9c4..2d94392 100644
--- a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -12,7 +12,7 @@
     method public final java.util.Collection<java.lang.Object> getData();
     method public final Object? getKey();
     method public final androidx.compose.ui.tooling.SourceLocation? getLocation();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public final String? getName();
     method public java.util.List<androidx.compose.ui.tooling.ParameterInformation> getParameters();
     property public final androidx.compose.ui.unit.IntBounds box;
@@ -20,7 +20,7 @@
     property public final java.util.Collection<java.lang.Object> data;
     property public final Object? key;
     property public final androidx.compose.ui.tooling.SourceLocation? location;
-    property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+    property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
     property public final String? name;
     property public java.util.List<androidx.compose.ui.tooling.ParameterInformation> parameters;
   }
@@ -41,9 +41,9 @@
   }
 
   public final class NodeGroup extends androidx.compose.ui.tooling.Group {
-    ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
+    ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
     method public Object getNode();
-    property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+    property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
     property public final Object node;
   }
 
@@ -74,7 +74,7 @@
   }
 
   public final class SlotTreeKt {
-    method public static androidx.compose.ui.tooling.Group asTree(androidx.compose.runtime.SlotTable);
+    method public static androidx.compose.ui.tooling.Group asTree(androidx.compose.runtime.CompositionData);
     method public static String? getPosition(androidx.compose.ui.tooling.Group);
   }
 
diff --git a/compose/ui/ui-tooling/api/restricted_current.txt b/compose/ui/ui-tooling/api/restricted_current.txt
index 23de9c4..2d94392 100644
--- a/compose/ui/ui-tooling/api/restricted_current.txt
+++ b/compose/ui/ui-tooling/api/restricted_current.txt
@@ -12,7 +12,7 @@
     method public final java.util.Collection<java.lang.Object> getData();
     method public final Object? getKey();
     method public final androidx.compose.ui.tooling.SourceLocation? getLocation();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public final String? getName();
     method public java.util.List<androidx.compose.ui.tooling.ParameterInformation> getParameters();
     property public final androidx.compose.ui.unit.IntBounds box;
@@ -20,7 +20,7 @@
     property public final java.util.Collection<java.lang.Object> data;
     property public final Object? key;
     property public final androidx.compose.ui.tooling.SourceLocation? location;
-    property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+    property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
     property public final String? name;
     property public java.util.List<androidx.compose.ui.tooling.ParameterInformation> parameters;
   }
@@ -41,9 +41,9 @@
   }
 
   public final class NodeGroup extends androidx.compose.ui.tooling.Group {
-    ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
+    ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
     method public Object getNode();
-    property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+    property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
     property public final Object node;
   }
 
@@ -74,7 +74,7 @@
   }
 
   public final class SlotTreeKt {
-    method public static androidx.compose.ui.tooling.Group asTree(androidx.compose.runtime.SlotTable);
+    method public static androidx.compose.ui.tooling.Group asTree(androidx.compose.runtime.CompositionData);
     method public static String? getPosition(androidx.compose.ui.tooling.Group);
   }
 
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/BoundsTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/BoundsTest.kt
index 5317df3..30d1dd4 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/BoundsTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/BoundsTest.kt
@@ -55,7 +55,7 @@
 
     @Test
     fun testBounds() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         show {
             Inspectable(slotTableRecord) {
                 Box {
@@ -98,7 +98,7 @@
 
     @Test
     fun testBoundWithConstraints() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         show {
             Inspectable(slotTableRecord) {
                 WithConstraints {
@@ -131,7 +131,7 @@
     @Test
     @LargeTest
     fun testDisposeWithComposeTables() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         var value by mutableStateOf(0)
         var latch = CountDownLatch(1)
         show {
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/InspectableTests.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/InspectableTests.kt
index 7f1b213..a1ea937 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/InspectableTests.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/InspectableTests.kt
@@ -25,7 +25,6 @@
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.SlotTable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.graphics.Color
@@ -50,7 +49,7 @@
 class InspectableTests : ToolingTest() {
     @Test
     fun simpleInspection() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         show {
             Inspectable(slotTableRecord) {
                 Column {
@@ -77,7 +76,7 @@
 
     @Test
     fun parametersTest() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         fun unknown(i: Int) = i
 
         show {
@@ -116,8 +115,8 @@
 
         val tree = slotTableRecord.store.first().asTree()
         val list = tree.asList()
-        val parameters = list.filter {
-            it.parameters.isNotEmpty() && it.location.let {
+        val parameters = list.filter { group ->
+            group.parameters.isNotEmpty() && group.location.let {
                 it != null && it.sourceFile == "InspectableTests.kt"
             }
         }
@@ -295,7 +294,7 @@
     fun inInspectionMode() {
         var displayed = false
         show {
-            Inspectable(SlotTableRecord.create()) {
+            Inspectable(CompositionDataRecord.create()) {
                 Column {
                     InInspectionModeOnly {
                         Box(Modifier.preferredSize(100.dp).background(color = Color(0xFF)))
@@ -326,7 +325,7 @@
     @InternalComposeApi
     @Test // regression test for b/161839910
     fun textParametersAreCorrect() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         show {
             Inspectable(slotTableRecord) {
                 Text("Test")
@@ -334,8 +333,8 @@
         }
         val tree = slotTableRecord.store.first().asTree()
         val list = tree.asList()
-        val parameters = list.filter {
-            it.parameters.isNotEmpty() && it.location.let {
+        val parameters = list.filter { group ->
+            group.parameters.isNotEmpty() && group.location.let {
                 it != null && it.sourceFile == "InspectableTests.kt"
             }
         }
@@ -431,14 +430,3 @@
     }
     return result
 }
-
-internal fun SlotTableRecord.findGroupForFile(fileName: String) =
-    store.map { it.findGroupForFile(fileName) }.filterNotNull().firstOrNull()
-
-@OptIn(InternalComposeApi::class)
-fun SlotTable.findGroupForFile(fileName: String) = asTree().findGroupForFile(fileName)
-fun Group.findGroupForFile(fileName: String): Group? {
-    val position = position
-    if (position != null && position.contains(fileName)) return this
-    return children.map { it.findGroupForFile(fileName) }.filterNotNull().firstOrNull()
-}
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt
index 6a7348f..a5070a6 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt
@@ -44,7 +44,7 @@
 
     @Test
     fun testBounds() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         show {
             Inspectable(slotTableRecord) {
                 with(AmbientDensity.current) {
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt
index bf3b298..f8c34a2 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt
@@ -21,12 +21,11 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.SlotTable
+import androidx.compose.runtime.CompositionData
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.R
 import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.platform.setContent
 import org.junit.Before
 import org.junit.Rule
@@ -73,17 +72,16 @@
         activityTestRule.onUiThread { }
     }
 
-    @OptIn(InternalComposeApi::class)
-    internal fun showAndRecord(content: @Composable () -> Unit): MutableSet<SlotTable>? {
+    internal fun showAndRecord(content: @Composable () -> Unit): MutableSet<CompositionData>? {
 
         positionedLatch = CountDownLatch(1)
-        val map: MutableSet<SlotTable> = Collections.newSetFromMap(
-            WeakHashMap<SlotTable, Boolean>()
+        val map: MutableSet<CompositionData> = Collections.newSetFromMap(
+            WeakHashMap<CompositionData, Boolean>()
         )
         activityTestRule.onUiThread {
-            AndroidOwner.onAndroidOwnerCreatedCallback = {
+            ViewRootForTest.onViewCreatedCallback = {
                 it.view.setTag(R.id.inspection_slot_table_set, map)
-                AndroidOwner.onAndroidOwnerCreatedCallback = null
+                ViewRootForTest.onViewCreatedCallback = null
             }
             activity.setContent {
                 Box(
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/InlineClassConverterTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/InlineClassConverterTest.kt
index 1491a4c..6a035f6 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/InlineClassConverterTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/InlineClassConverterTest.kt
@@ -21,7 +21,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.Group
 import androidx.compose.ui.tooling.Inspectable
-import androidx.compose.ui.tooling.SlotTableRecord
+import androidx.compose.ui.tooling.CompositionDataRecord
 import androidx.compose.ui.tooling.ToolingTest
 import androidx.compose.ui.tooling.asTree
 import androidx.compose.ui.unit.Dp
@@ -38,7 +38,7 @@
 
     @Test
     fun parameterValueTest() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         show {
             Inspectable(slotTableRecord) {
                 Surface {
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 73d6452..89c3db5 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.text.BasicText
 import androidx.compose.material.Button
 import androidx.compose.material.ModalDrawerLayout
 import androidx.compose.material.Surface
@@ -33,10 +34,12 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.tooling.Group
 import androidx.compose.ui.tooling.Inspectable
 import androidx.compose.ui.tooling.R
-import androidx.compose.ui.tooling.SlotTableRecord
+import androidx.compose.ui.tooling.CompositionDataRecord
 import androidx.compose.ui.tooling.ToolingTest
 import androidx.compose.ui.tooling.asTree
 import androidx.compose.ui.tooling.position
@@ -73,7 +76,7 @@
     @Ignore("Manual test")
     @Test
     fun buildTree() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
 
         show {
             Inspectable(slotTableRecord) {
@@ -385,7 +388,7 @@
 
     @Test
     fun testStitchTreeFromModelDrawerLayout() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
 
         show {
             Inspectable(slotTableRecord) {
@@ -439,7 +442,7 @@
 
     @Test
     fun testSpacer() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
 
         show {
             Inspectable(slotTableRecord) {
@@ -461,11 +464,35 @@
         assertThat(node).isNotNull()
     }
 
+    @Test // regression test b/174855322
+    fun testBasicText() {
+        val slotTableRecord = CompositionDataRecord.create()
+
+        view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
+        show {
+            Column {
+                BasicText(
+                    text = "Some text",
+                    style = TextStyle(textDecoration = TextDecoration.Underline)
+                )
+            }
+        }
+
+        val builder = LayoutInspectorTree()
+        val node = builder.convert(view)
+            .flatMap { flatten(it) }
+            .firstOrNull { it.name == "BasicText" }
+
+        assertThat(node).isNotNull()
+
+        assertThat(node?.parameters).isNotEmpty()
+    }
+
     @SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29:  b/171519437
     @Test
     @Ignore("b/174152464")
     fun testTextId() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
 
         show {
             Inspectable(slotTableRecord) {
@@ -637,7 +664,7 @@
             else -> value?.toString() ?: "null"
         }
 
-    private fun dumpSlotTableSet(slotTableRecord: SlotTableRecord) {
+    private fun dumpSlotTableSet(slotTableRecord: CompositionDataRecord) {
         @Suppress("ConstantConditionIf")
         if (!DEBUG) {
             return
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/OffsetInformationTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/OffsetInformationTest.kt
index 96c833c..7c85542 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/OffsetInformationTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/OffsetInformationTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.compose.ui.tooling.Group
 import androidx.compose.ui.tooling.Inspectable
-import androidx.compose.ui.tooling.SlotTableRecord
+import androidx.compose.ui.tooling.CompositionDataRecord
 import androidx.compose.ui.tooling.ToolingTest
 import androidx.compose.ui.tooling.asTree
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -32,7 +32,7 @@
 class OffsetInformationTest : ToolingTest() {
     @Test
     fun testOffset() {
-        val slotTableRecord = SlotTableRecord.create()
+        val slotTableRecord = CompositionDataRecord.create()
         show {
             Inspectable(slotTableRecord) {
                 OffsetData()
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/ParameterFactoryTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/ParameterFactoryTest.kt
index 8f590a8..70cf0f0 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/ParameterFactoryTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/ParameterFactoryTest.kt
@@ -40,8 +40,8 @@
 import androidx.compose.ui.draw.paint
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.LinearGradient
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.graphics.SolidColor
@@ -49,7 +49,6 @@
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextStyle
@@ -81,7 +80,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@ExperimentalLayoutNodeApi
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class ParameterFactoryTest {
@@ -200,7 +198,11 @@
             factory.create(
                 node,
                 "brush",
-                LinearGradient(listOf(Color.Red, Color.Blue), 0.0f, 0.5f, 5.0f, 10.0f)
+                Brush.linearGradient(
+                    colors = listOf(Color.Red, Color.Blue),
+                    start = Offset(0.0f, 0.5f),
+                    end = Offset(5.0f, 10.0f)
+                )
             )!!
         ) {
             parameter("brush", ParameterType.String, "LinearGradient") {
@@ -208,10 +210,17 @@
                     parameter("0", ParameterType.Color, Color.Red.toArgb())
                     parameter("1", ParameterType.Color, Color.Blue.toArgb())
                 }
-                parameter("endX", ParameterType.Float, 5.0f)
-                parameter("endY", ParameterType.Float, 10.0f)
-                parameter("startX", ParameterType.Float, 0.0f)
-                parameter("startY", ParameterType.Float, 0.5f)
+                // Parameters are traversed in alphabetical order through reflection queries.
+                // Validate createdSize exists before validating end parameter
+                parameter("createdSize", ParameterType.String, "Unspecified")
+                parameter("end", ParameterType.String, Offset::class.java.simpleName) {
+                    parameter("x", ParameterType.DimensionDp, 2.5f)
+                    parameter("y", ParameterType.DimensionDp, 5.0f)
+                }
+                parameter("start", ParameterType.String, Offset::class.java.simpleName) {
+                    parameter("x", ParameterType.DimensionDp, 0.0f)
+                    parameter("y", ParameterType.DimensionDp, 0.25f)
+                }
                 parameter("tileMode", ParameterType.String, "Clamp")
             }
         }
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
index 591a4fa..6c431b6 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
@@ -53,17 +53,30 @@
      * Asserts that the given Composable method executes correct and outputs some [ViewInfo]s.
      */
     private fun assertRendersCorrectly(className: String, methodName: String): List<ViewInfo> {
-        val committed = CountDownLatch(1)
+        val committedAndDrawn = CountDownLatch(1)
+        val committed = AtomicBoolean(false)
         activityTestRule.runOnUiThread {
             composeViewAdapter.init(
                 className, methodName, debugViewInfos = true,
                 onCommit = {
-                    committed.countDown()
+                    committed.set(true)
+                },
+                onDraw = {
+                    if (committed.get()) {
+                        committedAndDrawn.countDown()
+                    }
                 }
             )
         }
 
-        committed.await()
+        // Workaround for a problem described in b/174291742 where onLayout will not be called
+        // after composition for the first test in the test suite.
+        activityTestRule.runOnUiThread {
+            composeViewAdapter.requestLayout()
+        }
+
+        // Wait for the first draw after the Composable has been committed.
+        committedAndDrawn.await()
         activityTestRule.runOnUiThread {
             assertTrue(composeViewAdapter.viewInfos.isNotEmpty())
         }
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
index c74b740..e2ebdbd 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
 import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
 import androidx.compose.ui.tooling.test.R
-import org.junit.Assert
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -53,10 +52,6 @@
                 debugViewInfos = true
             )
         }
-
-        activityTestRule.runOnUiThread {
-            Assert.assertTrue(composeViewAdapter.viewInfos.isNotEmpty())
-        }
     }
 
     private class MyListProvider : CollectionPreviewParameterProvider<Int>(listOf(1, 2, 3))
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt
index d8145bc..b0d5680 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt
@@ -17,9 +17,9 @@
 package androidx.compose.ui.tooling
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionData
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.Providers
-import androidx.compose.runtime.SlotTable
 import androidx.compose.runtime.currentComposer
 import androidx.compose.runtime.tooling.InspectionTables
 import androidx.compose.ui.platform.InspectionMode
@@ -27,20 +27,19 @@
 import java.util.WeakHashMap
 
 /**
- * Storage for the preview generated [SlotTable]s.
+ * Storage for the preview generated [CompositionData]s.
  */
-internal interface SlotTableRecord {
-    @OptIn(InternalComposeApi::class)
-    val store: Set<SlotTable>
+internal interface CompositionDataRecord {
+    val store: Set<CompositionData>
 
     companion object {
-        fun create(): SlotTableRecord = SlotTableRecordImpl()
+        fun create(): CompositionDataRecord = CompositionDataRecordImpl()
     }
 }
 
-private class SlotTableRecordImpl : SlotTableRecord {
+private class CompositionDataRecordImpl : CompositionDataRecord {
     @OptIn(InternalComposeApi::class)
-    override val store: MutableSet<SlotTable> =
+    override val store: MutableSet<CompositionData> =
         Collections.newSetFromMap(WeakHashMap())
 }
 
@@ -48,19 +47,21 @@
  * A wrapper for compositions in inspection mode. The composition inside the Inspectable component
  * is in inspection mode.
  *
- * @param slotTableRecord [SlotTableRecord] to record the SlotTable used in the composition of [content]
+ * @param compositionDataRecord [CompositionDataRecord] to record the SlotTable used in the
+ * composition of [content]
  *
  * @suppress
  */
 @Composable
 @OptIn(InternalComposeApi::class)
 internal fun Inspectable(
-    slotTableRecord: SlotTableRecord,
+    compositionDataRecord: CompositionDataRecord,
     content: @Composable () -> Unit
 ) {
     currentComposer.collectKeySourceInformation()
-    val store = (slotTableRecord as SlotTableRecordImpl).store
-    store.add(currentComposer.slotTable)
+    currentComposer.collectParameterInformation()
+    val store = (compositionDataRecord as CompositionDataRecordImpl).store
+    store.add(currentComposer.compositionData)
     Providers(
         InspectionMode provides true,
         InspectionTables provides store,
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
index bb3361d..f38aa50 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
@@ -14,18 +14,13 @@
  * limitations under the License.
  */
 
-@file:OptIn(InternalComposeApi::class)
-
 package androidx.compose.ui.tooling
 
-import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.SlotReader
-import androidx.compose.runtime.SlotTable
-import androidx.compose.runtime.keySourceInfoOf
+import androidx.compose.runtime.CompositionData
+import androidx.compose.runtime.CompositionGroup
 import androidx.compose.ui.layout.globalPosition
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.ModifierInfo
+import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.layout.ModifierInfo
 import androidx.compose.ui.unit.IntBounds
 import java.lang.reflect.Field
 import kotlin.math.max
@@ -161,9 +156,6 @@
  */
 data class JoinedKey(val left: Any?, val right: Any?)
 
-@OptIn(InternalComposeApi::class)
-private fun convertKey(key: Int): Any? = keySourceInfoOf(key)
-
 internal val emptyBox = IntBounds(0, 0, 0, 0)
 
 private val tokenizer = Regex("(\\d+)|([,])|([*])|([:])|L|(P\\([^)]*\\))|(C(\\(([^)]*)\\))?)|@")
@@ -433,30 +425,17 @@
 /**
  * Iterate the slot table and extract a group tree that corresponds to the content of the table.
  */
-@OptIn(ExperimentalLayoutNodeApi::class, InternalComposeApi::class)
-private fun SlotReader.getGroup(parentContext: SourceInformationContext?): Group {
-    val key = convertKey(groupKey)
-    val groupData = groupAux
-    val context = if (groupData != null && groupData is String) {
-        sourceInformationContextOf(groupData, parentContext)
-    } else null
-    val nodeGroup = isNode
-    val end = currentGroup + groupSize
-    val node = if (nodeGroup) groupNode else null
+private fun CompositionGroup.getGroup(parentContext: SourceInformationContext?): Group {
+    val key = key
+    val context = sourceInfo?.let { sourceInformationContextOf(it, parentContext) }
+    val node = node
     val data = mutableListOf<Any?>()
     val children = mutableListOf<Group>()
-    for (index in 0 until groupSlotCount) {
-        data.add(groupGet(index))
-    }
+    data.addAll(this.data)
+    for (child in compositionGroups)
+        children.add(child.getGroup(context))
 
-    reposition(currentGroup + 1)
-
-    // A group ends with a list of groups
-    while (currentGroup < end) {
-        children.add(getGroup(context))
-    }
-
-    val modifierInfo = if (node is LayoutNode) {
+    val modifierInfo = if (node is LayoutInfo) {
         node.getModifierInfo()
     } else {
         emptyList()
@@ -464,14 +443,14 @@
 
     // Calculate bounding box
     val box = when (node) {
-        is LayoutNode -> boundsOfLayoutNode(node)
+        is LayoutInfo -> boundsOfLayoutNode(node)
         else ->
             if (children.isEmpty()) emptyBox else
                 children.map { g -> g.box }.reduce { acc, box -> box.union(acc) }
     }
-    return if (nodeGroup) NodeGroup(
+    return if (node != null) NodeGroup(
         key,
-        node as Any,
+        node,
         box,
         data,
         modifierInfo,
@@ -492,9 +471,8 @@
         )
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
-private fun boundsOfLayoutNode(node: LayoutNode): IntBounds {
-    if (node.owner == null) {
+private fun boundsOfLayoutNode(node: LayoutInfo): IntBounds {
+    if (!node.isAttached) {
         return IntBounds(
             left = 0,
             top = 0,
@@ -515,8 +493,7 @@
  * Return a group tree for for the slot table that represents the entire content of the slot
  * table.
  */
-@OptIn(InternalComposeApi::class)
-fun SlotTable.asTree(): Group = read { it.getGroup(null) }
+fun CompositionData.asTree(): Group = compositionGroups.first().getGroup(null)
 
 internal fun IntBounds.union(other: IntBounds): IntBounds {
     if (this == emptyBox) return other else if (other == emptyBox) return this
@@ -549,62 +526,61 @@
     context: SourceInformationContext?
 ): List<ParameterInformation> {
     if (data.isNotEmpty()) {
-        val recomposeScope = data[0]
+        val recomposeScope = data.firstOrNull {
+            it != null && it.javaClass.name.endsWith(recomposeScopeNameSuffix)
+        }
         if (recomposeScope != null) {
-            val scopeClass = recomposeScope.javaClass
-            if (scopeClass.name.endsWith(recomposeScopeNameSuffix)) {
-                try {
-                    val blockField = scopeClass.accessibleField("block")
-                    if (blockField != null) {
-                        val block = blockField.get(recomposeScope)
-                        if (block != null) {
-                            val blockClass = block.javaClass
-                            val defaultsField = blockClass.accessibleField(defaultFieldName)
-                            val changedField = blockClass.accessibleField(changedFieldName)
-                            val default =
-                                if (defaultsField != null) defaultsField.get(block) as Int else 0
-                            val changed =
-                                if (changedField != null) changedField.get(block) as Int else 0
-                            val fields = blockClass.declaredFields
-                                .filter {
-                                    it.name.startsWith(parameterPrefix) &&
-                                        !it.name.startsWith(internalFieldPrefix) &&
-                                        !it.name.startsWith(jacocoDataField)
-                                }.sortedBy { it.name }
-                            val parameters = mutableListOf<ParameterInformation>()
-                            val parametersMetadata = context?.parameters ?: emptyList()
-                            repeat(fields.size) { index ->
-                                val metadata = if (index < parametersMetadata.size)
-                                    parametersMetadata[index] else Parameter(index)
-                                if (metadata.sortedIndex >= fields.size) return@repeat
-                                val field = fields[metadata.sortedIndex]
-                                field.isAccessible = true
-                                val value = field.get(block)
-                                val fromDefault = (1 shl index) and default != 0
-                                val changedOffset = index * BITS_PER_SLOT + 1
-                                val parameterChanged = (
-                                    (SLOT_MASK shl changedOffset) and changed
-                                    ) shr changedOffset
-                                val static = parameterChanged and STATIC_BITS == STATIC_BITS
-                                val compared = parameterChanged and STATIC_BITS == 0
-                                val stable = parameterChanged and STABLE_BITS == 0
-                                parameters.add(
-                                    ParameterInformation(
-                                        name = field.name.substring(1),
-                                        value = value,
-                                        fromDefault = fromDefault,
-                                        static = static,
-                                        compared = compared && !fromDefault,
-                                        inlineClass = metadata.inlineClass,
-                                        stable = stable
-                                    )
+            try {
+                val blockField = recomposeScope.javaClass.accessibleField("block")
+                if (blockField != null) {
+                    val block = blockField.get(recomposeScope)
+                    if (block != null) {
+                        val blockClass = block.javaClass
+                        val defaultsField = blockClass.accessibleField(defaultFieldName)
+                        val changedField = blockClass.accessibleField(changedFieldName)
+                        val default =
+                            if (defaultsField != null) defaultsField.get(block) as Int else 0
+                        val changed =
+                            if (changedField != null) changedField.get(block) as Int else 0
+                        val fields = blockClass.declaredFields
+                            .filter {
+                                it.name.startsWith(parameterPrefix) &&
+                                    !it.name.startsWith(internalFieldPrefix) &&
+                                    !it.name.startsWith(jacocoDataField)
+                            }.sortedBy { it.name }
+                        val parameters = mutableListOf<ParameterInformation>()
+                        val parametersMetadata = context?.parameters ?: emptyList()
+                        repeat(fields.size) { index ->
+                            val metadata = if (index < parametersMetadata.size)
+                                parametersMetadata[index] else Parameter(index)
+                            if (metadata.sortedIndex >= fields.size) return@repeat
+                            val field = fields[metadata.sortedIndex]
+                            field.isAccessible = true
+                            val value = field.get(block)
+                            val fromDefault = (1 shl index) and default != 0
+                            val changedOffset = index * BITS_PER_SLOT + 1
+                            val parameterChanged = (
+                                (SLOT_MASK shl changedOffset) and changed
+                                ) shr changedOffset
+                            val static = parameterChanged and STATIC_BITS == STATIC_BITS
+                            val compared = parameterChanged and STATIC_BITS == 0
+                            val stable = parameterChanged and STABLE_BITS == 0
+                            parameters.add(
+                                ParameterInformation(
+                                    name = field.name.substring(1),
+                                    value = value,
+                                    fromDefault = fromDefault,
+                                    static = static,
+                                    compared = compared && !fromDefault,
+                                    inlineClass = metadata.inlineClass,
+                                    stable = stable
                                 )
-                            }
-                            return parameters
+                            )
                         }
+                        return parameters
                     }
-                } catch (_: Throwable) {
                 }
+            } catch (_: Throwable) {
             }
         }
     }
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt
index f42c9d5..c6add12 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt
@@ -16,8 +16,7 @@
 
 package androidx.compose.ui.tooling.inspector
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.layout.LayoutInfo
 
 /**
  * Node representing a Composable for the Layout Inspector.
@@ -105,10 +104,9 @@
 /**
  * Mutable version of [InspectorNode].
  */
-@ExperimentalLayoutNodeApi
 internal class MutableInspectorNode {
     var id = 0L
-    var layoutNodes = mutableListOf<LayoutNode>()
+    var layoutNodes = mutableListOf<LayoutInfo>()
     var name = ""
     var fileName = ""
     var packageHash = -1
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
index edad65e..f2acc87 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
@@ -17,9 +17,9 @@
 package androidx.compose.ui.tooling.inspector
 
 import android.view.View
+import androidx.compose.runtime.CompositionData
 import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.SlotTable
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
+import androidx.compose.ui.layout.LayoutInfo
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.OwnedLayer
 import androidx.compose.ui.tooling.Group
@@ -58,14 +58,13 @@
 /**
  * Generator of a tree for the Layout Inspector.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 class LayoutInspectorTree {
     private val inlineClassConverter = InlineClassConverter()
     private val parameterFactory = ParameterFactory(inlineClassConverter)
     private val cache = ArrayDeque<MutableInspectorNode>()
     private var generatedId = -1L
-    /** Map from [LayoutNode] to the nearest [InspectorNode] that contains it */
-    private val claimedNodes = IdentityHashMap<LayoutNode, InspectorNode>()
+    /** Map from [LayoutInfo] to the nearest [InspectorNode] that contains it */
+    private val claimedNodes = IdentityHashMap<LayoutInfo, InspectorNode>()
     /** Map from parent tree to child trees that are about to be stitched together */
     private val treeMap = IdentityHashMap<MutableInspectorNode, MutableList<MutableInspectorNode>>()
     /** Map from owner node to child trees that are about to be stitched to this owner */
@@ -75,13 +74,13 @@
         Collections.newSetFromMap(IdentityHashMap<MutableInspectorNode, Boolean>())
 
     /**
-     * Converts the [SlotTable] set held by [view] into a list of root nodes.
+     * Converts the [CompositionData] set held by [view] into a list of root nodes.
      */
     @OptIn(InternalComposeApi::class)
     fun convert(view: View): List<InspectorNode> {
         parameterFactory.density = Density(view.context)
         @Suppress("UNCHECKED_CAST")
-        val tables = view.getTag(R.id.inspection_slot_table_set) as? Set<SlotTable>
+        val tables = view.getTag(R.id.inspection_slot_table_set) as? Set<CompositionData>
             ?: return emptyList()
         clear()
         val result = convert(tables)
@@ -113,7 +112,7 @@
     }
 
     @OptIn(InternalComposeApi::class)
-    private fun convert(tables: Set<SlotTable>): List<InspectorNode> {
+    private fun convert(tables: Set<CompositionData>): List<InspectorNode> {
         val trees = tables.map { convert(it) }
         return when (trees.size) {
             0 -> listOf()
@@ -123,20 +122,21 @@
     }
 
     /**
-     * Stitch separate trees together using the [LayoutNode]s found in the [SlotTable]s.
+     * Stitch separate trees together using the [LayoutNode]s found in the [CompositionData]s.
      *
-     * Some constructs in Compose (e.g. ModalDrawerLayout) will result is multiple [SlotTable]s.
-     * This code will attempt to stitch the resulting [InspectorNode] trees together by looking
-     * at the parent of each [LayoutNode].
+     * Some constructs in Compose (e.g. ModalDrawerLayout) will result is multiple
+     * [CompositionData]s. This code will attempt to stitch the resulting [InspectorNode] trees
+     * together by looking at the parent of each [LayoutNode].
+     *
      * If this algorithm is successful the result of this function will be a list with a single
      * tree.
      */
     private fun stitchTreesByLayoutNode(trees: List<MutableInspectorNode>): List<InspectorNode> {
-        val layoutToTreeMap = IdentityHashMap<LayoutNode, MutableInspectorNode>()
+        val layoutToTreeMap = IdentityHashMap<LayoutInfo, MutableInspectorNode>()
         trees.forEach { tree -> tree.layoutNodes.forEach { layoutToTreeMap[it] = tree } }
         trees.forEach { tree ->
             val layout = tree.layoutNodes.lastOrNull()
-            val parentLayout = generateSequence(layout) { it.parent }.firstOrNull {
+            val parentLayout = generateSequence(layout) { it.parentInfo }.firstOrNull {
                 val otherTree = layoutToTreeMap[it]
                 otherTree != null && otherTree != tree
             }
@@ -201,7 +201,7 @@
     }
 
     @OptIn(InternalComposeApi::class)
-    private fun convert(table: SlotTable): MutableInspectorNode {
+    private fun convert(table: CompositionData): MutableInspectorNode {
         val fakeParent = newNode()
         addToParent(fakeParent, listOf(convert(table.asTree())))
         return fakeParent
@@ -264,7 +264,7 @@
     private fun parse(group: Group): MutableInspectorNode {
         val node = newNode()
         node.id = getRenderNode(group)
-        ((group as? NodeGroup)?.node as? LayoutNode)?.let { node.layoutNodes.add(it) }
+        ((group as? NodeGroup)?.node as? LayoutInfo)?.let { node.layoutNodes.add(it) }
         if (!parseCallLocation(group, node) && group.name.isNullOrEmpty()) {
             return markUnwanted(node)
         }
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/ParameterFactory.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/ParameterFactory.kt
index 18f3d85..ddbd8f4 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/ParameterFactory.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/ParameterFactory.kt
@@ -30,7 +30,6 @@
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.font.FontListFontFamily
@@ -70,7 +69,6 @@
  *
  * Each parameter value is converted to a user readable value.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class ParameterFactory(private val inlineClassConverter: InlineClassConverter) {
     /**
      * A map from known values to a user readable string representation.
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
index c72068b..99eb764 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
@@ -40,12 +40,12 @@
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.platform.AmbientFontLoader
-import androidx.compose.ui.platform.AndroidOwner
 import androidx.compose.ui.platform.AnimationClockAmbient
 import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.tooling.Group
 import androidx.compose.ui.tooling.Inspectable
-import androidx.compose.ui.tooling.SlotTableRecord
+import androidx.compose.ui.tooling.CompositionDataRecord
 import androidx.compose.ui.tooling.SourceLocation
 import androidx.compose.ui.tooling.asTree
 import androidx.compose.ui.tooling.preview.animation.PreviewAnimationClock
@@ -126,7 +126,7 @@
      */
     private var debugPaintBounds = false
     internal var viewInfos: List<ViewInfo> = emptyList()
-    private val slotTableRecord = SlotTableRecord.create()
+    private val slotTableRecord = CompositionDataRecord.create()
 
     /**
      * Simple function name of the Composable being previewed.
@@ -255,6 +255,7 @@
             throw exception
         }
 
+        processViewInfos()
         if (composableName.isNotEmpty()) {
             // TODO(b/160126628): support other APIs, e.g. animate
             findAndSubscribeTransitions()
@@ -423,7 +424,6 @@
 
         previewComposition = @Composable {
             onCommit {
-                processViewInfos()
                 onCommit()
             }
 
@@ -461,7 +461,8 @@
                         // (an AndroidOwner) when setting the clock time to make sure the Compose
                         // Preview will animate when the states are read inside the draw scope.
                         val composeView = getChildAt(0) as ComposeView
-                        (composeView.getChildAt(0) as? AndroidOwner)?.invalidateDescendants()
+                        (composeView.getChildAt(0) as? ViewRootForTest)
+                            ?.invalidateDescendants()
                     }
                     Providers(AmbientAnimationClock provides clock) {
                         composable()
diff --git a/compose/ui/ui-util/api/current.txt b/compose/ui/ui-util/api/current.txt
index 8f93c6c..7f36a788 100644
--- a/compose/ui/ui-util/api/current.txt
+++ b/compose/ui/ui-util/api/current.txt
@@ -34,10 +34,6 @@
     method public static Object nativeClass(Object);
   }
 
-  public final class JvmSynchronizationHelperKt {
-    method public static <T> T! synchronized(Object lock, kotlin.jvm.functions.Function0<? extends T> block);
-  }
-
   public final class ListUtilsKt {
     method public static inline <T> boolean fastAll(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static inline <T> boolean fastAny(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
@@ -55,8 +51,6 @@
     method public static int lerp(int start, int stop, float fraction);
     method public static long lerp(long start, long stop, float fraction);
     method public static String toHexString(int);
-    method public static float toRadians(float);
-    method public static double toRadians(double);
   }
 
 }
diff --git a/compose/ui/ui-util/api/public_plus_experimental_current.txt b/compose/ui/ui-util/api/public_plus_experimental_current.txt
index 8f93c6c..7f36a788 100644
--- a/compose/ui/ui-util/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-util/api/public_plus_experimental_current.txt
@@ -34,10 +34,6 @@
     method public static Object nativeClass(Object);
   }
 
-  public final class JvmSynchronizationHelperKt {
-    method public static <T> T! synchronized(Object lock, kotlin.jvm.functions.Function0<? extends T> block);
-  }
-
   public final class ListUtilsKt {
     method public static inline <T> boolean fastAll(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static inline <T> boolean fastAny(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
@@ -55,8 +51,6 @@
     method public static int lerp(int start, int stop, float fraction);
     method public static long lerp(long start, long stop, float fraction);
     method public static String toHexString(int);
-    method public static float toRadians(float);
-    method public static double toRadians(double);
   }
 
 }
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index 8f93c6c..7f36a788 100644
--- a/compose/ui/ui-util/api/restricted_current.txt
+++ b/compose/ui/ui-util/api/restricted_current.txt
@@ -34,10 +34,6 @@
     method public static Object nativeClass(Object);
   }
 
-  public final class JvmSynchronizationHelperKt {
-    method public static <T> T! synchronized(Object lock, kotlin.jvm.functions.Function0<? extends T> block);
-  }
-
   public final class ListUtilsKt {
     method public static inline <T> boolean fastAll(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static inline <T> boolean fastAny(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
@@ -55,8 +51,6 @@
     method public static int lerp(int start, int stop, float fraction);
     method public static long lerp(long start, long stop, float fraction);
     method public static String toHexString(int);
-    method public static float toRadians(float);
-    method public static double toRadians(double);
   }
 
 }
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
index 7650fd4..8a87c71 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
@@ -15,7 +15,6 @@
  */
 package androidx.compose.ui.util
 
-import kotlin.math.PI
 import kotlin.math.roundToInt
 import kotlin.math.roundToLong
 
@@ -44,6 +43,3 @@
 
 @OptIn(kotlin.ExperimentalUnsignedTypes::class)
 fun Int.toHexString() = "0x${toUInt().toString(16).padStart(8, '0')}"
-
-fun Float.toRadians(): Float = this / 180f * PI.toFloat()
-fun Double.toRadians(): Double = this / 180 * PI
diff --git a/compose/ui/ui-util/src/jvmMain/kotlin/androidx/compose/ui/util/JvmSynchronizationHelper.kt b/compose/ui/ui-util/src/jvmMain/kotlin/androidx/compose/ui/util/JvmSynchronizationHelper.kt
deleted file mode 100644
index f94d1ec..0000000
--- a/compose/ui/ui-util/src/jvmMain/kotlin/androidx/compose/ui/util/JvmSynchronizationHelper.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.util
-
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-
-/**
- * [kotlin.synchronized][synchronized] is deprecated, and the build fails if we use
- * [kotlin.synchronized][synchronized] along with the IR compiler. As a workaround, we have this
- * function here, which is in a module that doesn't use the IR Compiler.
- */
-@OptIn(ExperimentalContracts::class)
-fun <T> synchronized(lock: Any, block: () -> T): T {
-    contract {
-        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
-    }
-    return kotlin.synchronized(lock, block)
-}
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index ee2bf84..23ce341 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -119,8 +119,7 @@
     method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
   }
 
-  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
-    method public void drawContent();
+  public final class ContentDrawScopeKt {
   }
 
   public final class DrawLayerModifierKt {
@@ -130,29 +129,32 @@
   public final class DrawModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
+  }
+
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
   }
 
   public final class FocusModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
+  @Deprecated public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+    method @Deprecated public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
     property public abstract kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange;
   }
 
   public final class FocusObserverModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
+    method @Deprecated public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+  public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
     method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
     property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
   }
 
   public final class FocusRequesterModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
+    method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
   }
 
   @androidx.compose.runtime.Stable public interface Modifier {
@@ -195,17 +197,17 @@
   public final class AndroidAutofillTypeKt {
   }
 
-  public interface Autofill {
+  @androidx.compose.ui.ExperimentalComposeUiApi public interface Autofill {
     method public void cancelAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
     method public void requestAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
   }
 
-  public final class AutofillNode {
+  @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillNode {
     ctor public AutofillNode(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
     method public java.util.List<androidx.compose.ui.autofill.AutofillType> component1();
     method public androidx.compose.ui.geometry.Rect? component2();
     method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? component3();
-    method public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
     method public java.util.List<androidx.compose.ui.autofill.AutofillType> getAutofillTypes();
     method public androidx.compose.ui.geometry.Rect? getBoundingBox();
     method public int getId();
@@ -217,7 +219,7 @@
     property public final kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? onFill;
   }
 
-  public final class AutofillTree {
+  @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillTree {
     ctor public AutofillTree();
     method public java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> getChildren();
     method public kotlin.Unit? performAutofill(int id, String value);
@@ -225,7 +227,7 @@
     property public final java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> children;
   }
 
-  public enum AutofillType {
+  @androidx.compose.ui.ExperimentalComposeUiApi public enum AutofillType {
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressAuxiliaryDetails;
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressCountry;
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressLocality;
@@ -279,7 +281,7 @@
     method public float getFontScale();
     method public long getSize-NH-jbRc();
     method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
     property public float density;
     property public float fontScale;
     property public final long size;
@@ -295,13 +297,13 @@
   }
 
   public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
-    method public void draw(androidx.compose.ui.ContentDrawScope);
+    method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
   }
 
   public final class DrawModifierKt {
     method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class DrawResult {
@@ -329,27 +331,61 @@
 
 package androidx.compose.ui.focus {
 
-  @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
+  public final class FocusChangedModifierKt {
+    method public static androidx.compose.ui.Modifier onFocusChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChanged);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+  public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
+    method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+  }
+
+  public final class FocusEventModifierKt {
+    method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
+  }
+
+  public interface FocusManager {
     method public void clearFocus(optional boolean forcedClear);
   }
 
   public final class FocusNodeUtilsKt {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public final class FocusRequester {
+  public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
     method public boolean freeFocus();
     method public void requestFocus();
+    field public static final androidx.compose.ui.focus.FocusRequester.Companion Companion;
+  }
+
+  public static final class FocusRequester.Companion {
+    method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+  }
+
+  public static final class FocusRequester.Companion.FocusRequesterFactory {
+    method public operator androidx.compose.ui.focus.FocusRequester component1();
+    method public operator androidx.compose.ui.focus.FocusRequester component10();
+    method public operator androidx.compose.ui.focus.FocusRequester component11();
+    method public operator androidx.compose.ui.focus.FocusRequester component12();
+    method public operator androidx.compose.ui.focus.FocusRequester component13();
+    method public operator androidx.compose.ui.focus.FocusRequester component14();
+    method public operator androidx.compose.ui.focus.FocusRequester component15();
+    method public operator androidx.compose.ui.focus.FocusRequester component16();
+    method public operator androidx.compose.ui.focus.FocusRequester component2();
+    method public operator androidx.compose.ui.focus.FocusRequester component3();
+    method public operator androidx.compose.ui.focus.FocusRequester component4();
+    method public operator androidx.compose.ui.focus.FocusRequester component5();
+    method public operator androidx.compose.ui.focus.FocusRequester component6();
+    method public operator androidx.compose.ui.focus.FocusRequester component7();
+    method public operator androidx.compose.ui.focus.FocusRequester component8();
+    method public operator androidx.compose.ui.focus.FocusRequester component9();
+    field public static final androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory INSTANCE;
   }
 
   public final class FocusRequesterKt {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public enum FocusState {
+  public enum FocusState {
     enum_constant public static final androidx.compose.ui.focus.FocusState Active;
     enum_constant public static final androidx.compose.ui.focus.FocusState ActiveParent;
     enum_constant public static final androidx.compose.ui.focus.FocusState Captured;
@@ -411,9 +447,6 @@
     method public static androidx.compose.ui.Modifier dragSlopExceededGestureFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> onDragSlopExceeded, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean>? canDrag, optional androidx.compose.ui.gesture.scrollorientationlocking.Orientation? orientation);
   }
 
-  @kotlin.RequiresOptIn(message="This pointer input API is experimental and is likely to change before becoming " + "stable.") public @interface ExperimentalPointerInput {
-  }
-
   public final class GestureUtilsKt {
     method public static boolean anyPointersInBounds-5eFHUEc(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange>, long bounds);
   }
@@ -494,11 +527,11 @@
 
 package androidx.compose.ui.gesture.customevents {
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
+  public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
     ctor public DelayUpEvent(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
     method public androidx.compose.ui.gesture.customevents.DelayUpMessage component1();
     method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> component2();
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
+    method public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
     method public androidx.compose.ui.gesture.customevents.DelayUpMessage getMessage();
     method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> getPointers();
     method public void setMessage(androidx.compose.ui.gesture.customevents.DelayUpMessage p);
@@ -506,7 +539,7 @@
     property public final java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public enum DelayUpMessage {
+  public enum DelayUpMessage {
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayUp;
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpConsumed;
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpNotConsumed;
@@ -518,6 +551,37 @@
 
 }
 
+package androidx.compose.ui.gesture.nestedscroll {
+
+  public interface NestedScrollConnection {
+    method public default void onPostFling-Pv53iXo(long consumed, long available, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Velocity,kotlin.Unit> onFinished);
+    method public default long onPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+    method public default long onPreFling-TH1AsA0(long available);
+    method public default long onPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+  }
+
+  public final class NestedScrollDelegatingWrapperKt {
+  }
+
+  public final class NestedScrollDispatcher {
+    ctor public NestedScrollDispatcher();
+    method public void dispatchPostFling-uYzo7IE(long consumed, long available);
+    method public long dispatchPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+    method public long dispatchPreFling-TH1AsA0(long available);
+    method public long dispatchPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+  }
+
+  public final class NestedScrollModifierKt {
+    method public static androidx.compose.ui.Modifier nestedScroll(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection connection, optional androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher? dispatcher);
+  }
+
+  public enum NestedScrollSource {
+    enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Drag;
+    enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Fling;
+  }
+
+}
+
 package androidx.compose.ui.gesture.scrollorientationlocking {
 
   public enum Orientation {
@@ -525,7 +589,7 @@
     enum_constant public static final androidx.compose.ui.gesture.scrollorientationlocking.Orientation Vertical;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public final class ScrollOrientationLocker {
+  public final class ScrollOrientationLocker {
     ctor public ScrollOrientationLocker(androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher);
     method public void attemptToLockPointers(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
     method public java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> getPointersFor(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
@@ -679,7 +743,8 @@
 
   public final class VectorApplier extends androidx.compose.runtime.AbstractApplier<androidx.compose.ui.graphics.vector.VNode> {
     ctor public VectorApplier(androidx.compose.ui.graphics.vector.VNode root);
-    method public void insert(int index, androidx.compose.ui.graphics.vector.VNode instance);
+    method public void insertBottomUp(int index, androidx.compose.ui.graphics.vector.VNode instance);
+    method public void insertTopDown(int index, androidx.compose.ui.graphics.vector.VNode instance);
     method public void move(int from, int to, int count);
     method protected void onClear();
     method public void remove(int index, int count);
@@ -812,18 +877,6 @@
 
 package androidx.compose.ui.input.key {
 
-  @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
-    method @Deprecated public boolean isLeftAltPressed();
-    method @Deprecated public default boolean isPressed();
-    method @Deprecated public boolean isRightAltPressed();
-    property public abstract boolean isLeftAltPressed;
-    property public default boolean isPressed;
-    property public abstract boolean isRightAltPressed;
-  }
-
-  @kotlin.RequiresOptIn(message="The Key Input API is experimental and is likely to change in the future.") public @interface ExperimentalKeyInput {
-  }
-
   public final inline class Key {
     ctor public Key();
     method public static int constructor-impl(int keyCode);
@@ -1417,8 +1470,7 @@
     property public final int ZoomOut;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
-    method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
+  public interface KeyEvent {
     method public int getKey-EK5gGoQ();
     method public androidx.compose.ui.input.key.KeyEventType getType();
     method public int getUtf16CodePoint();
@@ -1426,7 +1478,6 @@
     method public boolean isCtrlPressed();
     method public boolean isMetaPressed();
     method public boolean isShiftPressed();
-    property @Deprecated public abstract androidx.compose.ui.input.key.Alt alt;
     property public abstract boolean isAltPressed;
     property public abstract boolean isCtrlPressed;
     property public abstract boolean isMetaPressed;
@@ -1436,15 +1487,17 @@
     property public abstract int utf16CodePoint;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public enum KeyEventType {
+  public enum KeyEventType {
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyDown;
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyUp;
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType Unknown;
   }
 
   public final class KeyInputModifierKt {
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+    method @Deprecated public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+    method public static androidx.compose.ui.Modifier onKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+    method public static androidx.compose.ui.Modifier onPreviewKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+    method @Deprecated public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
   }
 
 }
@@ -1455,6 +1508,16 @@
     method public static boolean isMouseInput();
   }
 
+  @kotlin.coroutines.RestrictsSuspension public interface AwaitPointerEventScope extends androidx.compose.ui.unit.Density {
+    method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
+    method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
+    method public long getSize-YbymL2g();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
+    property public abstract long size;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+  }
+
   public final class ConsumedData {
     method public boolean getDownChange();
     method public long getPositionChange-F1C5BW0();
@@ -1473,17 +1536,6 @@
     method public void retainHitPaths(java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointerIds);
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
-    method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
-    method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
-    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputData> getCurrentPointers();
-    method public long getSize-YbymL2g();
-    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    property public abstract java.util.List<androidx.compose.ui.input.pointer.PointerInputData> currentPointers;
-    property public abstract long size;
-    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
-  }
-
   public final class HitPathTrackerKt {
   }
 
@@ -1601,12 +1653,11 @@
     property public abstract androidx.compose.ui.input.pointer.PointerInputFilter pointerInputFilter;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
-    method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
+  public interface PointerInputScope extends androidx.compose.ui.unit.Density {
+    method public suspend <R> Object? awaitPointerEventScope(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R> p);
     method public long getSize-YbymL2g();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
-    property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
+    method @Deprecated public default suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1627,7 +1678,7 @@
   }
 
   public final class SuspendingPointerInputFilterKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+    method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
   }
 
 }
@@ -1740,14 +1791,30 @@
     property public abstract Object layoutId;
   }
 
+  public interface LayoutInfo {
+    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+    method public int getHeight();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
+    method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+    method public int getWidth();
+    method public boolean isAttached();
+    method public boolean isPlaced();
+    property public abstract androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public abstract int height;
+    property public abstract boolean isAttached;
+    property public abstract boolean isPlaced;
+    property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public abstract int width;
+  }
+
   public final class LayoutKt {
     method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.node.ExperimentalLayoutNodeApi public static inline void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocks, optional androidx.compose.ui.Modifier modifier);
-    method public static androidx.compose.ui.node.LayoutNode.MeasureBlocks MeasuringIntrinsicsMeasureBlocks(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
+    method @androidx.compose.runtime.Composable public static inline void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, androidx.compose.ui.node.MeasureBlocks measureBlocks, optional androidx.compose.ui.Modifier modifier);
+    method public static androidx.compose.ui.node.MeasureBlocks MeasuringIntrinsicsMeasureBlocks(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @Deprecated @androidx.compose.runtime.Composable public static void MultiMeasureLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function0<kotlin.Unit> children, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @androidx.compose.runtime.Composable public static void WithConstraints(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.WithConstraintsScope,kotlin.Unit> content);
-    method @androidx.compose.ui.node.ExperimentalLayoutNodeApi public static androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocksOf(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
+    method public static androidx.compose.ui.node.MeasureBlocks measureBlocksOf(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
   }
 
   public interface LayoutModifier extends androidx.compose.ui.Modifier.Element {
@@ -1795,6 +1862,16 @@
     method public static inline String! toString-impl(androidx.compose.ui.layout.Placeable! p);
   }
 
+  public final class ModifierInfo {
+    ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
+    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+    method public Object? getExtra();
+    method public androidx.compose.ui.Modifier getModifier();
+    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public final Object? extra;
+    property public final androidx.compose.ui.Modifier modifier;
+  }
+
   public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
     method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
   }
@@ -1900,6 +1977,9 @@
     method public java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class TestModifierUpdaterKt {
+  }
+
   public final class VerticalAlignmentLine extends androidx.compose.ui.layout.AlignmentLine {
     ctor public VerticalAlignmentLine(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> merger);
   }
@@ -1921,38 +2001,18 @@
 
 package androidx.compose.ui.node {
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalLayoutNodeApi {
-  }
-
   @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface InternalCoreApi {
   }
 
-  @androidx.compose.ui.node.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
-    ctor public LayoutNode();
-    method public void attach(androidx.compose.ui.node.Owner owner);
-    method public void detach();
-    method public void draw(androidx.compose.ui.graphics.Canvas canvas);
+  public final class LayoutNode implements androidx.compose.ui.layout.LayoutInfo androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
     method public void forceRemeasure();
-    method public Integer? getAlignmentLine(androidx.compose.ui.layout.AlignmentLine line);
-    method @Deprecated public boolean getCanMultiMeasure();
-    method public java.util.List<androidx.compose.ui.node.LayoutNode> getChildren();
     method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
-    method public androidx.compose.ui.unit.Density getDensity();
-    method public int getDepth();
     method public int getHeight();
-    method public androidx.compose.ui.node.LayoutNode.MeasureBlocks getMeasureBlocks();
-    method public androidx.compose.ui.layout.MeasureScope getMeasureScope();
-    method public androidx.compose.ui.Modifier getModifier();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? getOnAttach();
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? getOnDetach();
-    method public androidx.compose.ui.node.Owner? getOwner();
-    method public androidx.compose.ui.node.LayoutNode? getParent();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public Object? getParentData();
+    method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
     method public int getWidth();
-    method public void hitTest-fhABUH0(long pointerPositionRelativeToScreen, java.util.List<androidx.compose.ui.input.pointer.PointerInputFilter> hitPointerInputFilters);
-    method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void insertAt(int index, androidx.compose.ui.node.LayoutNode instance);
+    method public boolean isAttached();
     method public boolean isPlaced();
     method public boolean isValid();
     method public int maxIntrinsicHeight(int width);
@@ -1960,39 +2020,20 @@
     method public androidx.compose.ui.layout.Placeable measure-BRTryo0(long constraints);
     method public int minIntrinsicHeight(int width);
     method public int minIntrinsicWidth(int height);
-    method public void move(int from, int to, int count);
-    method public void place(int x, int y);
-    method public void removeAll();
-    method public void removeAt(int index, int count);
-    method public void requestRelayout();
-    method public void requestRemeasure();
-    method @Deprecated public void setCanMultiMeasure(boolean p);
-    method public void setDensity(androidx.compose.ui.unit.Density p);
-    method public void setDepth(int p);
-    method public void setMeasureBlocks(androidx.compose.ui.node.LayoutNode.MeasureBlocks value);
-    method public void setModifier(androidx.compose.ui.Modifier value);
-    method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.Owner,kotlin.Unit>? p);
-    method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.Owner,kotlin.Unit>? p);
-    property @Deprecated public final boolean canMultiMeasure;
-    property public final java.util.List<androidx.compose.ui.node.LayoutNode> children;
-    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
-    property public final androidx.compose.ui.unit.Density density;
-    property public final int depth;
-    property public final int height;
-    property public final boolean isPlaced;
+    property public androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public int height;
+    property public boolean isAttached;
+    property public boolean isPlaced;
     property public boolean isValid;
-    property public final androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocks;
-    property public final androidx.compose.ui.layout.MeasureScope measureScope;
-    property public final androidx.compose.ui.Modifier modifier;
-    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? onAttach;
-    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? onDetach;
-    property public final androidx.compose.ui.node.Owner? owner;
-    property public final androidx.compose.ui.node.LayoutNode? parent;
     property public Object? parentData;
-    property public final int width;
+    property public androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public int width;
   }
 
-  public static interface LayoutNode.MeasureBlocks {
+  public final class LayoutNodeKt {
+  }
+
+  public interface MeasureBlocks {
     method public int maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
     method public int maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
     method public androidx.compose.ui.layout.MeasureResult measure-8A2P9vY(androidx.compose.ui.layout.MeasureScope measureScope, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, long constraints);
@@ -2000,28 +2041,6 @@
     method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
   }
 
-  public abstract static class LayoutNode.NoIntrinsicsMeasureBlocks implements androidx.compose.ui.node.LayoutNode.MeasureBlocks {
-    ctor public LayoutNode.NoIntrinsicsMeasureBlocks(String error);
-    method public Void maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
-    method public Void maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
-    method public Void minIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
-    method public Void minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
-  }
-
-  public final class LayoutNodeKt {
-    method public static androidx.compose.ui.node.LayoutNode? findClosestParentNode(androidx.compose.ui.node.LayoutNode, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.LayoutNode,java.lang.Boolean> selector);
-  }
-
-  public final class ModifierInfo {
-    ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
-    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
-    method public Object? getExtra();
-    method public androidx.compose.ui.Modifier getModifier();
-    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
-    property public final Object? extra;
-    property public final androidx.compose.ui.Modifier modifier;
-  }
-
   public interface OwnedLayer {
     method public void destroy();
     method public void drawLayer(androidx.compose.ui.graphics.Canvas canvas);
@@ -2045,7 +2064,6 @@
     method public androidx.compose.ui.focus.FocusManager getFocusManager();
     method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
     method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
-    method public boolean getHasPendingMeasureOrLayout();
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
     method public long getMeasureIteration();
     method public androidx.compose.ui.node.LayoutNode getRoot();
@@ -2063,7 +2081,7 @@
     method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
     method public void onSemanticsChange();
     method public boolean requestFocus();
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
+    method public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
     property public abstract androidx.compose.ui.autofill.Autofill? autofill;
     property public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
     property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
@@ -2071,7 +2089,6 @@
     property public abstract androidx.compose.ui.focus.FocusManager focusManager;
     property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
     property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
-    property public abstract boolean hasPendingMeasureOrLayout;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
     property public abstract long measureIteration;
     property public abstract androidx.compose.ui.node.LayoutNode root;
@@ -2107,16 +2124,13 @@
     property public final T? value;
   }
 
-  public final class UiApplier implements androidx.compose.runtime.Applier<java.lang.Object> {
+  public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
     ctor public UiApplier(Object root);
-    method public void clear();
-    method public void down(Object node);
-    method public Object getCurrent();
-    method public void insert(int index, Object instance);
+    method public void insertBottomUp(int index, Object instance);
+    method public void insertTopDown(int index, Object instance);
     method public void move(int from, int to, int count);
+    method protected void onClear();
     method public void remove(int index, int count);
-    method public void up();
-    property public Object current;
   }
 
   public final class ViewInteropKt {
@@ -2154,8 +2168,6 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
-    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
-    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.focus.FocusManager>! getFocusManagerAmbient();
@@ -2187,31 +2199,6 @@
   public final class AndroidComposeViewKt {
   }
 
-  public interface AndroidOwner extends androidx.compose.ui.node.Owner {
-    method @androidx.compose.ui.node.ExperimentalLayoutNodeApi public void addAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, androidx.compose.ui.node.LayoutNode layoutNode);
-    method public void drawAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, android.graphics.Canvas canvas);
-    method public kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> getConfigurationChangeObserver();
-    method public android.view.View getView();
-    method public androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? getViewTreeOwners();
-    method public void invalidateDescendants();
-    method public void removeAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view);
-    method public void setConfigurationChangeObserver(kotlin.jvm.functions.Function1<? super android.content.res.Configuration,kotlin.Unit> p);
-    method public void setOnViewTreeOwnersAvailable(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners,kotlin.Unit> callback);
-    property public abstract kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> configurationChangeObserver;
-    property public abstract android.view.View view;
-    property public abstract androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? viewTreeOwners;
-  }
-
-  public static final class AndroidOwner.ViewTreeOwners {
-    ctor public AndroidOwner.ViewTreeOwners(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner);
-    method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
-    method public androidx.savedstate.SavedStateRegistryOwner getSavedStateRegistryOwner();
-    method public androidx.lifecycle.ViewModelStoreOwner getViewModelStoreOwner();
-    property public final androidx.lifecycle.LifecycleOwner lifecycleOwner;
-    property public final androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner;
-    property public final androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner;
-  }
-
   public final class AndroidUriHandler implements androidx.compose.ui.platform.UriHandler {
     ctor public AndroidUriHandler(android.content.Context context);
     method public void openUri(String uri);
@@ -2290,10 +2277,6 @@
   public final class JvmActualsKt {
   }
 
-  public final class SubcompositionKt {
-    method @MainThread public static androidx.compose.runtime.Composition subcomposeInto(androidx.compose.ui.node.LayoutNode container, androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
-  }
-
   public final class TestTagKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier testTag(androidx.compose.ui.Modifier, String tag);
   }
@@ -2345,6 +2328,23 @@
     property public abstract float touchSlop;
   }
 
+  @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+    method public boolean getHasPendingMeasureOrLayout();
+    method public android.view.View getView();
+    method public void invalidateDescendants();
+    method public boolean isLifecycleInResumedState();
+    property public abstract boolean hasPendingMeasureOrLayout;
+    property public abstract boolean isLifecycleInResumedState;
+    property public abstract android.view.View view;
+    field public static final androidx.compose.ui.platform.ViewRootForTest.Companion Companion;
+  }
+
+  public static final class ViewRootForTest.Companion {
+    method public kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? getOnViewCreatedCallback();
+    method public void setOnViewCreatedCallback(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? p);
+    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
+  }
+
   @androidx.compose.runtime.Stable public interface WindowManager {
     method public boolean isWindowFocused();
     property public abstract boolean isWindowFocused;
@@ -2402,6 +2402,10 @@
     ctor public LoadedResource(T? resource);
   }
 
+  public final class PainterResourcesKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+  }
+
   public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
     ctor public PendingResource(T? resource);
   }
@@ -2436,7 +2440,7 @@
 
 package androidx.compose.ui.selection {
 
-  public interface Selectable {
+  @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
     method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
     method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
     method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2486,9 +2490,11 @@
   public final class SelectionManagerKt {
   }
 
-  public interface SelectionRegistrar {
-    method public void onPositionChange();
-    method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+  @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+    method public void notifyPositionChange();
+    method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+    method public void notifySelectionUpdateEnd();
+    method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
     method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
     method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
   }
@@ -2602,12 +2608,13 @@
     method public operator <T> T! get(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key);
     method public <T> T! getOrElse(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
     method public <T> T? getOrElseNullable(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
-    method public boolean isEmpty();
+    method public boolean isClearingSemantics();
     method public boolean isMergingSemanticsOfDescendants();
     method public java.util.Iterator<java.util.Map.Entry<androidx.compose.ui.semantics.SemanticsPropertyKey<?>,java.lang.Object>> iterator();
     method public <T> void set(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, T? value);
+    method public void setClearingSemantics(boolean p);
     method public void setMergingSemanticsOfDescendants(boolean p);
-    property public final boolean isEmpty;
+    property public final boolean isClearingSemantics;
     property public final boolean isMergingSemanticsOfDescendants;
   }
 
@@ -2623,6 +2630,7 @@
   }
 
   public final class SemanticsModifierKt {
+    method public static androidx.compose.ui.Modifier clearAndSetSemantics(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> properties);
     method public static androidx.compose.ui.Modifier semantics(androidx.compose.ui.Modifier, optional boolean mergeDescendants, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> properties);
   }
 
@@ -2630,25 +2638,27 @@
     method public int getAlignmentLinePosition(androidx.compose.ui.layout.AlignmentLine line);
     method public androidx.compose.ui.geometry.Rect getBoundsInRoot();
     method public java.util.List<androidx.compose.ui.semantics.SemanticsNode> getChildren();
-    method public androidx.compose.ui.node.LayoutNode getComponentNode();
     method public androidx.compose.ui.semantics.SemanticsConfiguration getConfig();
     method public androidx.compose.ui.geometry.Rect getGlobalBounds();
     method public long getGlobalPosition-F1C5BW0();
     method public int getId();
+    method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
     method public boolean getMergingEnabled();
+    method public androidx.compose.ui.node.Owner? getOwner();
     method public androidx.compose.ui.semantics.SemanticsNode? getParent();
     method public long getPositionInRoot-F1C5BW0();
     method public long getSize-YbymL2g();
     method public boolean isRoot();
     property public final androidx.compose.ui.geometry.Rect boundsInRoot;
     property public final java.util.List<androidx.compose.ui.semantics.SemanticsNode> children;
-    property public final androidx.compose.ui.node.LayoutNode componentNode;
     property public final androidx.compose.ui.semantics.SemanticsConfiguration config;
     property public final androidx.compose.ui.geometry.Rect globalBounds;
     property public final long globalPosition;
     property public final int id;
     property public final boolean isRoot;
+    property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
     property public final boolean mergingEnabled;
+    property public final androidx.compose.ui.node.Owner? owner;
     property public final androidx.compose.ui.semantics.SemanticsNode? parent;
     property public final long positionInRoot;
     property public final long size;
@@ -2658,7 +2668,6 @@
   }
 
   public final class SemanticsOwner {
-    ctor public SemanticsOwner(androidx.compose.ui.node.LayoutNode rootNode);
     method public androidx.compose.ui.node.LayoutNode getRootNode();
     method public androidx.compose.ui.semantics.SemanticsNode getRootSemanticsNode();
     method public androidx.compose.ui.semantics.SemanticsNode getUnmergedRootSemanticsNode();
@@ -2672,9 +2681,8 @@
   }
 
   public final class SemanticsProperties {
-    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityLabel();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> getAccessibilityRangeInfo();
-    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityValue();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getContentDescription();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getDisabled();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHidden();
@@ -2683,14 +2691,14 @@
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsPopup();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getSelected();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getStateDescription();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getTestTag();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> getText();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> getTextSelectionRange();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.state.ToggleableState> getToggleableState();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityScrollState> getVerticalAccessibilityScrollState();
-    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityLabel;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> AccessibilityRangeInfo;
-    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityValue;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> ContentDescription;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Disabled;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Hidden;
@@ -2699,6 +2707,7 @@
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsPopup;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Selected;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> StateDescription;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> TestTag;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> Text;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> TextSelectionRange;
@@ -2713,14 +2722,16 @@
     method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
-    method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
-    method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
-    method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method @Deprecated public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method @Deprecated public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static String getContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> getCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static boolean getFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.semantics.AccessibilityScrollState getHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.text.input.ImeAction getImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static boolean getSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static String getStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static String getTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.text.AnnotatedString getText(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void getTextLayoutResult(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean> action);
@@ -2733,9 +2744,9 @@
     method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> action);
-    method public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
-    method public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
-    method public static void setAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
+    method @Deprecated public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method @Deprecated public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method public static void setContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
     method public static void setCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> p);
     method public static void setFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
     method public static void setHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityScrollState p);
@@ -2743,6 +2754,8 @@
     method public static void setProgress(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> action);
     method public static void setSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
     method public static void setSelection(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Integer,? super java.lang.Boolean,java.lang.Boolean> action);
+    method public static void setStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method public static void setStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
     method public static void setTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
     method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.text.AnnotatedString p);
     method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean> action);
@@ -2833,11 +2846,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class AndroidDialogProperties implements androidx.compose.ui.window.DialogProperties {
-    ctor public AndroidDialogProperties(androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    ctor public AndroidDialogProperties(boolean dismissOnBackPress, boolean dismissOnClickOutside, androidx.compose.ui.window.SecureFlagPolicy securePolicy);
     ctor public AndroidDialogProperties();
-    method public androidx.compose.ui.window.SecureFlagPolicy component1();
-    method @androidx.compose.runtime.Immutable public androidx.compose.ui.window.AndroidDialogProperties copy(androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    method public boolean component1();
+    method public boolean component2();
+    method public androidx.compose.ui.window.SecureFlagPolicy component3();
+    method @androidx.compose.runtime.Immutable public androidx.compose.ui.window.AndroidDialogProperties copy(boolean dismissOnBackPress, boolean dismissOnClickOutside, androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    method public boolean getDismissOnBackPress();
+    method public boolean getDismissOnClickOutside();
     method public androidx.compose.ui.window.SecureFlagPolicy getSecurePolicy();
+    property public final boolean dismissOnBackPress;
+    property public final boolean dismissOnClickOutside;
     property public final androidx.compose.ui.window.SecureFlagPolicy securePolicy;
   }
 
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index ee2bf84..23ce341 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -119,8 +119,7 @@
     method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
   }
 
-  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
-    method public void drawContent();
+  public final class ContentDrawScopeKt {
   }
 
   public final class DrawLayerModifierKt {
@@ -130,29 +129,32 @@
   public final class DrawModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
+  }
+
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
   }
 
   public final class FocusModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
+  @Deprecated public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+    method @Deprecated public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
     property public abstract kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange;
   }
 
   public final class FocusObserverModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
+    method @Deprecated public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+  public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
     method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
     property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
   }
 
   public final class FocusRequesterModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
+    method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
   }
 
   @androidx.compose.runtime.Stable public interface Modifier {
@@ -195,17 +197,17 @@
   public final class AndroidAutofillTypeKt {
   }
 
-  public interface Autofill {
+  @androidx.compose.ui.ExperimentalComposeUiApi public interface Autofill {
     method public void cancelAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
     method public void requestAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
   }
 
-  public final class AutofillNode {
+  @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillNode {
     ctor public AutofillNode(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
     method public java.util.List<androidx.compose.ui.autofill.AutofillType> component1();
     method public androidx.compose.ui.geometry.Rect? component2();
     method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? component3();
-    method public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
     method public java.util.List<androidx.compose.ui.autofill.AutofillType> getAutofillTypes();
     method public androidx.compose.ui.geometry.Rect? getBoundingBox();
     method public int getId();
@@ -217,7 +219,7 @@
     property public final kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? onFill;
   }
 
-  public final class AutofillTree {
+  @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillTree {
     ctor public AutofillTree();
     method public java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> getChildren();
     method public kotlin.Unit? performAutofill(int id, String value);
@@ -225,7 +227,7 @@
     property public final java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> children;
   }
 
-  public enum AutofillType {
+  @androidx.compose.ui.ExperimentalComposeUiApi public enum AutofillType {
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressAuxiliaryDetails;
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressCountry;
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressLocality;
@@ -279,7 +281,7 @@
     method public float getFontScale();
     method public long getSize-NH-jbRc();
     method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
     property public float density;
     property public float fontScale;
     property public final long size;
@@ -295,13 +297,13 @@
   }
 
   public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
-    method public void draw(androidx.compose.ui.ContentDrawScope);
+    method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
   }
 
   public final class DrawModifierKt {
     method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class DrawResult {
@@ -329,27 +331,61 @@
 
 package androidx.compose.ui.focus {
 
-  @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
+  public final class FocusChangedModifierKt {
+    method public static androidx.compose.ui.Modifier onFocusChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChanged);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+  public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
+    method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+  }
+
+  public final class FocusEventModifierKt {
+    method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
+  }
+
+  public interface FocusManager {
     method public void clearFocus(optional boolean forcedClear);
   }
 
   public final class FocusNodeUtilsKt {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public final class FocusRequester {
+  public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
     method public boolean freeFocus();
     method public void requestFocus();
+    field public static final androidx.compose.ui.focus.FocusRequester.Companion Companion;
+  }
+
+  public static final class FocusRequester.Companion {
+    method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+  }
+
+  public static final class FocusRequester.Companion.FocusRequesterFactory {
+    method public operator androidx.compose.ui.focus.FocusRequester component1();
+    method public operator androidx.compose.ui.focus.FocusRequester component10();
+    method public operator androidx.compose.ui.focus.FocusRequester component11();
+    method public operator androidx.compose.ui.focus.FocusRequester component12();
+    method public operator androidx.compose.ui.focus.FocusRequester component13();
+    method public operator androidx.compose.ui.focus.FocusRequester component14();
+    method public operator androidx.compose.ui.focus.FocusRequester component15();
+    method public operator androidx.compose.ui.focus.FocusRequester component16();
+    method public operator androidx.compose.ui.focus.FocusRequester component2();
+    method public operator androidx.compose.ui.focus.FocusRequester component3();
+    method public operator androidx.compose.ui.focus.FocusRequester component4();
+    method public operator androidx.compose.ui.focus.FocusRequester component5();
+    method public operator androidx.compose.ui.focus.FocusRequester component6();
+    method public operator androidx.compose.ui.focus.FocusRequester component7();
+    method public operator androidx.compose.ui.focus.FocusRequester component8();
+    method public operator androidx.compose.ui.focus.FocusRequester component9();
+    field public static final androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory INSTANCE;
   }
 
   public final class FocusRequesterKt {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public enum FocusState {
+  public enum FocusState {
     enum_constant public static final androidx.compose.ui.focus.FocusState Active;
     enum_constant public static final androidx.compose.ui.focus.FocusState ActiveParent;
     enum_constant public static final androidx.compose.ui.focus.FocusState Captured;
@@ -411,9 +447,6 @@
     method public static androidx.compose.ui.Modifier dragSlopExceededGestureFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> onDragSlopExceeded, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean>? canDrag, optional androidx.compose.ui.gesture.scrollorientationlocking.Orientation? orientation);
   }
 
-  @kotlin.RequiresOptIn(message="This pointer input API is experimental and is likely to change before becoming " + "stable.") public @interface ExperimentalPointerInput {
-  }
-
   public final class GestureUtilsKt {
     method public static boolean anyPointersInBounds-5eFHUEc(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange>, long bounds);
   }
@@ -494,11 +527,11 @@
 
 package androidx.compose.ui.gesture.customevents {
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
+  public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
     ctor public DelayUpEvent(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
     method public androidx.compose.ui.gesture.customevents.DelayUpMessage component1();
     method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> component2();
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
+    method public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
     method public androidx.compose.ui.gesture.customevents.DelayUpMessage getMessage();
     method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> getPointers();
     method public void setMessage(androidx.compose.ui.gesture.customevents.DelayUpMessage p);
@@ -506,7 +539,7 @@
     property public final java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public enum DelayUpMessage {
+  public enum DelayUpMessage {
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayUp;
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpConsumed;
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpNotConsumed;
@@ -518,6 +551,37 @@
 
 }
 
+package androidx.compose.ui.gesture.nestedscroll {
+
+  public interface NestedScrollConnection {
+    method public default void onPostFling-Pv53iXo(long consumed, long available, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Velocity,kotlin.Unit> onFinished);
+    method public default long onPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+    method public default long onPreFling-TH1AsA0(long available);
+    method public default long onPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+  }
+
+  public final class NestedScrollDelegatingWrapperKt {
+  }
+
+  public final class NestedScrollDispatcher {
+    ctor public NestedScrollDispatcher();
+    method public void dispatchPostFling-uYzo7IE(long consumed, long available);
+    method public long dispatchPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+    method public long dispatchPreFling-TH1AsA0(long available);
+    method public long dispatchPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+  }
+
+  public final class NestedScrollModifierKt {
+    method public static androidx.compose.ui.Modifier nestedScroll(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection connection, optional androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher? dispatcher);
+  }
+
+  public enum NestedScrollSource {
+    enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Drag;
+    enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Fling;
+  }
+
+}
+
 package androidx.compose.ui.gesture.scrollorientationlocking {
 
   public enum Orientation {
@@ -525,7 +589,7 @@
     enum_constant public static final androidx.compose.ui.gesture.scrollorientationlocking.Orientation Vertical;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public final class ScrollOrientationLocker {
+  public final class ScrollOrientationLocker {
     ctor public ScrollOrientationLocker(androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher);
     method public void attemptToLockPointers(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
     method public java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> getPointersFor(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
@@ -679,7 +743,8 @@
 
   public final class VectorApplier extends androidx.compose.runtime.AbstractApplier<androidx.compose.ui.graphics.vector.VNode> {
     ctor public VectorApplier(androidx.compose.ui.graphics.vector.VNode root);
-    method public void insert(int index, androidx.compose.ui.graphics.vector.VNode instance);
+    method public void insertBottomUp(int index, androidx.compose.ui.graphics.vector.VNode instance);
+    method public void insertTopDown(int index, androidx.compose.ui.graphics.vector.VNode instance);
     method public void move(int from, int to, int count);
     method protected void onClear();
     method public void remove(int index, int count);
@@ -812,18 +877,6 @@
 
 package androidx.compose.ui.input.key {
 
-  @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
-    method @Deprecated public boolean isLeftAltPressed();
-    method @Deprecated public default boolean isPressed();
-    method @Deprecated public boolean isRightAltPressed();
-    property public abstract boolean isLeftAltPressed;
-    property public default boolean isPressed;
-    property public abstract boolean isRightAltPressed;
-  }
-
-  @kotlin.RequiresOptIn(message="The Key Input API is experimental and is likely to change in the future.") public @interface ExperimentalKeyInput {
-  }
-
   public final inline class Key {
     ctor public Key();
     method public static int constructor-impl(int keyCode);
@@ -1417,8 +1470,7 @@
     property public final int ZoomOut;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
-    method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
+  public interface KeyEvent {
     method public int getKey-EK5gGoQ();
     method public androidx.compose.ui.input.key.KeyEventType getType();
     method public int getUtf16CodePoint();
@@ -1426,7 +1478,6 @@
     method public boolean isCtrlPressed();
     method public boolean isMetaPressed();
     method public boolean isShiftPressed();
-    property @Deprecated public abstract androidx.compose.ui.input.key.Alt alt;
     property public abstract boolean isAltPressed;
     property public abstract boolean isCtrlPressed;
     property public abstract boolean isMetaPressed;
@@ -1436,15 +1487,17 @@
     property public abstract int utf16CodePoint;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public enum KeyEventType {
+  public enum KeyEventType {
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyDown;
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyUp;
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType Unknown;
   }
 
   public final class KeyInputModifierKt {
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+    method @Deprecated public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+    method public static androidx.compose.ui.Modifier onKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+    method public static androidx.compose.ui.Modifier onPreviewKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+    method @Deprecated public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
   }
 
 }
@@ -1455,6 +1508,16 @@
     method public static boolean isMouseInput();
   }
 
+  @kotlin.coroutines.RestrictsSuspension public interface AwaitPointerEventScope extends androidx.compose.ui.unit.Density {
+    method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
+    method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
+    method public long getSize-YbymL2g();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
+    property public abstract long size;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+  }
+
   public final class ConsumedData {
     method public boolean getDownChange();
     method public long getPositionChange-F1C5BW0();
@@ -1473,17 +1536,6 @@
     method public void retainHitPaths(java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointerIds);
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
-    method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
-    method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
-    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputData> getCurrentPointers();
-    method public long getSize-YbymL2g();
-    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    property public abstract java.util.List<androidx.compose.ui.input.pointer.PointerInputData> currentPointers;
-    property public abstract long size;
-    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
-  }
-
   public final class HitPathTrackerKt {
   }
 
@@ -1601,12 +1653,11 @@
     property public abstract androidx.compose.ui.input.pointer.PointerInputFilter pointerInputFilter;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
-    method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
+  public interface PointerInputScope extends androidx.compose.ui.unit.Density {
+    method public suspend <R> Object? awaitPointerEventScope(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R> p);
     method public long getSize-YbymL2g();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
-    property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
+    method @Deprecated public default suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1627,7 +1678,7 @@
   }
 
   public final class SuspendingPointerInputFilterKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+    method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
   }
 
 }
@@ -1740,14 +1791,30 @@
     property public abstract Object layoutId;
   }
 
+  public interface LayoutInfo {
+    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+    method public int getHeight();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
+    method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+    method public int getWidth();
+    method public boolean isAttached();
+    method public boolean isPlaced();
+    property public abstract androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public abstract int height;
+    property public abstract boolean isAttached;
+    property public abstract boolean isPlaced;
+    property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public abstract int width;
+  }
+
   public final class LayoutKt {
     method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.node.ExperimentalLayoutNodeApi public static inline void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocks, optional androidx.compose.ui.Modifier modifier);
-    method public static androidx.compose.ui.node.LayoutNode.MeasureBlocks MeasuringIntrinsicsMeasureBlocks(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
+    method @androidx.compose.runtime.Composable public static inline void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, androidx.compose.ui.node.MeasureBlocks measureBlocks, optional androidx.compose.ui.Modifier modifier);
+    method public static androidx.compose.ui.node.MeasureBlocks MeasuringIntrinsicsMeasureBlocks(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @Deprecated @androidx.compose.runtime.Composable public static void MultiMeasureLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function0<kotlin.Unit> children, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @androidx.compose.runtime.Composable public static void WithConstraints(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.WithConstraintsScope,kotlin.Unit> content);
-    method @androidx.compose.ui.node.ExperimentalLayoutNodeApi public static androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocksOf(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
+    method public static androidx.compose.ui.node.MeasureBlocks measureBlocksOf(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
   }
 
   public interface LayoutModifier extends androidx.compose.ui.Modifier.Element {
@@ -1795,6 +1862,16 @@
     method public static inline String! toString-impl(androidx.compose.ui.layout.Placeable! p);
   }
 
+  public final class ModifierInfo {
+    ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
+    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+    method public Object? getExtra();
+    method public androidx.compose.ui.Modifier getModifier();
+    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public final Object? extra;
+    property public final androidx.compose.ui.Modifier modifier;
+  }
+
   public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
     method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
   }
@@ -1900,6 +1977,9 @@
     method public java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class TestModifierUpdaterKt {
+  }
+
   public final class VerticalAlignmentLine extends androidx.compose.ui.layout.AlignmentLine {
     ctor public VerticalAlignmentLine(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> merger);
   }
@@ -1921,38 +2001,18 @@
 
 package androidx.compose.ui.node {
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalLayoutNodeApi {
-  }
-
   @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface InternalCoreApi {
   }
 
-  @androidx.compose.ui.node.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
-    ctor public LayoutNode();
-    method public void attach(androidx.compose.ui.node.Owner owner);
-    method public void detach();
-    method public void draw(androidx.compose.ui.graphics.Canvas canvas);
+  public final class LayoutNode implements androidx.compose.ui.layout.LayoutInfo androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
     method public void forceRemeasure();
-    method public Integer? getAlignmentLine(androidx.compose.ui.layout.AlignmentLine line);
-    method @Deprecated public boolean getCanMultiMeasure();
-    method public java.util.List<androidx.compose.ui.node.LayoutNode> getChildren();
     method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
-    method public androidx.compose.ui.unit.Density getDensity();
-    method public int getDepth();
     method public int getHeight();
-    method public androidx.compose.ui.node.LayoutNode.MeasureBlocks getMeasureBlocks();
-    method public androidx.compose.ui.layout.MeasureScope getMeasureScope();
-    method public androidx.compose.ui.Modifier getModifier();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? getOnAttach();
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? getOnDetach();
-    method public androidx.compose.ui.node.Owner? getOwner();
-    method public androidx.compose.ui.node.LayoutNode? getParent();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public Object? getParentData();
+    method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
     method public int getWidth();
-    method public void hitTest-fhABUH0(long pointerPositionRelativeToScreen, java.util.List<androidx.compose.ui.input.pointer.PointerInputFilter> hitPointerInputFilters);
-    method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void insertAt(int index, androidx.compose.ui.node.LayoutNode instance);
+    method public boolean isAttached();
     method public boolean isPlaced();
     method public boolean isValid();
     method public int maxIntrinsicHeight(int width);
@@ -1960,39 +2020,20 @@
     method public androidx.compose.ui.layout.Placeable measure-BRTryo0(long constraints);
     method public int minIntrinsicHeight(int width);
     method public int minIntrinsicWidth(int height);
-    method public void move(int from, int to, int count);
-    method public void place(int x, int y);
-    method public void removeAll();
-    method public void removeAt(int index, int count);
-    method public void requestRelayout();
-    method public void requestRemeasure();
-    method @Deprecated public void setCanMultiMeasure(boolean p);
-    method public void setDensity(androidx.compose.ui.unit.Density p);
-    method public void setDepth(int p);
-    method public void setMeasureBlocks(androidx.compose.ui.node.LayoutNode.MeasureBlocks value);
-    method public void setModifier(androidx.compose.ui.Modifier value);
-    method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.Owner,kotlin.Unit>? p);
-    method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.Owner,kotlin.Unit>? p);
-    property @Deprecated public final boolean canMultiMeasure;
-    property public final java.util.List<androidx.compose.ui.node.LayoutNode> children;
-    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
-    property public final androidx.compose.ui.unit.Density density;
-    property public final int depth;
-    property public final int height;
-    property public final boolean isPlaced;
+    property public androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public int height;
+    property public boolean isAttached;
+    property public boolean isPlaced;
     property public boolean isValid;
-    property public final androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocks;
-    property public final androidx.compose.ui.layout.MeasureScope measureScope;
-    property public final androidx.compose.ui.Modifier modifier;
-    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? onAttach;
-    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? onDetach;
-    property public final androidx.compose.ui.node.Owner? owner;
-    property public final androidx.compose.ui.node.LayoutNode? parent;
     property public Object? parentData;
-    property public final int width;
+    property public androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public int width;
   }
 
-  public static interface LayoutNode.MeasureBlocks {
+  public final class LayoutNodeKt {
+  }
+
+  public interface MeasureBlocks {
     method public int maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
     method public int maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
     method public androidx.compose.ui.layout.MeasureResult measure-8A2P9vY(androidx.compose.ui.layout.MeasureScope measureScope, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, long constraints);
@@ -2000,28 +2041,6 @@
     method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
   }
 
-  public abstract static class LayoutNode.NoIntrinsicsMeasureBlocks implements androidx.compose.ui.node.LayoutNode.MeasureBlocks {
-    ctor public LayoutNode.NoIntrinsicsMeasureBlocks(String error);
-    method public Void maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
-    method public Void maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
-    method public Void minIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
-    method public Void minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
-  }
-
-  public final class LayoutNodeKt {
-    method public static androidx.compose.ui.node.LayoutNode? findClosestParentNode(androidx.compose.ui.node.LayoutNode, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.LayoutNode,java.lang.Boolean> selector);
-  }
-
-  public final class ModifierInfo {
-    ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
-    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
-    method public Object? getExtra();
-    method public androidx.compose.ui.Modifier getModifier();
-    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
-    property public final Object? extra;
-    property public final androidx.compose.ui.Modifier modifier;
-  }
-
   public interface OwnedLayer {
     method public void destroy();
     method public void drawLayer(androidx.compose.ui.graphics.Canvas canvas);
@@ -2045,7 +2064,6 @@
     method public androidx.compose.ui.focus.FocusManager getFocusManager();
     method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
     method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
-    method public boolean getHasPendingMeasureOrLayout();
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
     method public long getMeasureIteration();
     method public androidx.compose.ui.node.LayoutNode getRoot();
@@ -2063,7 +2081,7 @@
     method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
     method public void onSemanticsChange();
     method public boolean requestFocus();
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
+    method public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
     property public abstract androidx.compose.ui.autofill.Autofill? autofill;
     property public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
     property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
@@ -2071,7 +2089,6 @@
     property public abstract androidx.compose.ui.focus.FocusManager focusManager;
     property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
     property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
-    property public abstract boolean hasPendingMeasureOrLayout;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
     property public abstract long measureIteration;
     property public abstract androidx.compose.ui.node.LayoutNode root;
@@ -2107,16 +2124,13 @@
     property public final T? value;
   }
 
-  public final class UiApplier implements androidx.compose.runtime.Applier<java.lang.Object> {
+  public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
     ctor public UiApplier(Object root);
-    method public void clear();
-    method public void down(Object node);
-    method public Object getCurrent();
-    method public void insert(int index, Object instance);
+    method public void insertBottomUp(int index, Object instance);
+    method public void insertTopDown(int index, Object instance);
     method public void move(int from, int to, int count);
+    method protected void onClear();
     method public void remove(int index, int count);
-    method public void up();
-    property public Object current;
   }
 
   public final class ViewInteropKt {
@@ -2154,8 +2168,6 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
-    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
-    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.focus.FocusManager>! getFocusManagerAmbient();
@@ -2187,31 +2199,6 @@
   public final class AndroidComposeViewKt {
   }
 
-  public interface AndroidOwner extends androidx.compose.ui.node.Owner {
-    method @androidx.compose.ui.node.ExperimentalLayoutNodeApi public void addAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, androidx.compose.ui.node.LayoutNode layoutNode);
-    method public void drawAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, android.graphics.Canvas canvas);
-    method public kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> getConfigurationChangeObserver();
-    method public android.view.View getView();
-    method public androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? getViewTreeOwners();
-    method public void invalidateDescendants();
-    method public void removeAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view);
-    method public void setConfigurationChangeObserver(kotlin.jvm.functions.Function1<? super android.content.res.Configuration,kotlin.Unit> p);
-    method public void setOnViewTreeOwnersAvailable(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners,kotlin.Unit> callback);
-    property public abstract kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> configurationChangeObserver;
-    property public abstract android.view.View view;
-    property public abstract androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? viewTreeOwners;
-  }
-
-  public static final class AndroidOwner.ViewTreeOwners {
-    ctor public AndroidOwner.ViewTreeOwners(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner);
-    method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
-    method public androidx.savedstate.SavedStateRegistryOwner getSavedStateRegistryOwner();
-    method public androidx.lifecycle.ViewModelStoreOwner getViewModelStoreOwner();
-    property public final androidx.lifecycle.LifecycleOwner lifecycleOwner;
-    property public final androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner;
-    property public final androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner;
-  }
-
   public final class AndroidUriHandler implements androidx.compose.ui.platform.UriHandler {
     ctor public AndroidUriHandler(android.content.Context context);
     method public void openUri(String uri);
@@ -2290,10 +2277,6 @@
   public final class JvmActualsKt {
   }
 
-  public final class SubcompositionKt {
-    method @MainThread public static androidx.compose.runtime.Composition subcomposeInto(androidx.compose.ui.node.LayoutNode container, androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
-  }
-
   public final class TestTagKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier testTag(androidx.compose.ui.Modifier, String tag);
   }
@@ -2345,6 +2328,23 @@
     property public abstract float touchSlop;
   }
 
+  @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+    method public boolean getHasPendingMeasureOrLayout();
+    method public android.view.View getView();
+    method public void invalidateDescendants();
+    method public boolean isLifecycleInResumedState();
+    property public abstract boolean hasPendingMeasureOrLayout;
+    property public abstract boolean isLifecycleInResumedState;
+    property public abstract android.view.View view;
+    field public static final androidx.compose.ui.platform.ViewRootForTest.Companion Companion;
+  }
+
+  public static final class ViewRootForTest.Companion {
+    method public kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? getOnViewCreatedCallback();
+    method public void setOnViewCreatedCallback(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? p);
+    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
+  }
+
   @androidx.compose.runtime.Stable public interface WindowManager {
     method public boolean isWindowFocused();
     property public abstract boolean isWindowFocused;
@@ -2402,6 +2402,10 @@
     ctor public LoadedResource(T? resource);
   }
 
+  public final class PainterResourcesKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+  }
+
   public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
     ctor public PendingResource(T? resource);
   }
@@ -2436,7 +2440,7 @@
 
 package androidx.compose.ui.selection {
 
-  public interface Selectable {
+  @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
     method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
     method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
     method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2486,9 +2490,11 @@
   public final class SelectionManagerKt {
   }
 
-  public interface SelectionRegistrar {
-    method public void onPositionChange();
-    method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+  @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+    method public void notifyPositionChange();
+    method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+    method public void notifySelectionUpdateEnd();
+    method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
     method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
     method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
   }
@@ -2602,12 +2608,13 @@
     method public operator <T> T! get(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key);
     method public <T> T! getOrElse(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
     method public <T> T? getOrElseNullable(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
-    method public boolean isEmpty();
+    method public boolean isClearingSemantics();
     method public boolean isMergingSemanticsOfDescendants();
     method public java.util.Iterator<java.util.Map.Entry<androidx.compose.ui.semantics.SemanticsPropertyKey<?>,java.lang.Object>> iterator();
     method public <T> void set(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, T? value);
+    method public void setClearingSemantics(boolean p);
     method public void setMergingSemanticsOfDescendants(boolean p);
-    property public final boolean isEmpty;
+    property public final boolean isClearingSemantics;
     property public final boolean isMergingSemanticsOfDescendants;
   }
 
@@ -2623,6 +2630,7 @@
   }
 
   public final class SemanticsModifierKt {
+    method public static androidx.compose.ui.Modifier clearAndSetSemantics(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> properties);
     method public static androidx.compose.ui.Modifier semantics(androidx.compose.ui.Modifier, optional boolean mergeDescendants, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> properties);
   }
 
@@ -2630,25 +2638,27 @@
     method public int getAlignmentLinePosition(androidx.compose.ui.layout.AlignmentLine line);
     method public androidx.compose.ui.geometry.Rect getBoundsInRoot();
     method public java.util.List<androidx.compose.ui.semantics.SemanticsNode> getChildren();
-    method public androidx.compose.ui.node.LayoutNode getComponentNode();
     method public androidx.compose.ui.semantics.SemanticsConfiguration getConfig();
     method public androidx.compose.ui.geometry.Rect getGlobalBounds();
     method public long getGlobalPosition-F1C5BW0();
     method public int getId();
+    method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
     method public boolean getMergingEnabled();
+    method public androidx.compose.ui.node.Owner? getOwner();
     method public androidx.compose.ui.semantics.SemanticsNode? getParent();
     method public long getPositionInRoot-F1C5BW0();
     method public long getSize-YbymL2g();
     method public boolean isRoot();
     property public final androidx.compose.ui.geometry.Rect boundsInRoot;
     property public final java.util.List<androidx.compose.ui.semantics.SemanticsNode> children;
-    property public final androidx.compose.ui.node.LayoutNode componentNode;
     property public final androidx.compose.ui.semantics.SemanticsConfiguration config;
     property public final androidx.compose.ui.geometry.Rect globalBounds;
     property public final long globalPosition;
     property public final int id;
     property public final boolean isRoot;
+    property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
     property public final boolean mergingEnabled;
+    property public final androidx.compose.ui.node.Owner? owner;
     property public final androidx.compose.ui.semantics.SemanticsNode? parent;
     property public final long positionInRoot;
     property public final long size;
@@ -2658,7 +2668,6 @@
   }
 
   public final class SemanticsOwner {
-    ctor public SemanticsOwner(androidx.compose.ui.node.LayoutNode rootNode);
     method public androidx.compose.ui.node.LayoutNode getRootNode();
     method public androidx.compose.ui.semantics.SemanticsNode getRootSemanticsNode();
     method public androidx.compose.ui.semantics.SemanticsNode getUnmergedRootSemanticsNode();
@@ -2672,9 +2681,8 @@
   }
 
   public final class SemanticsProperties {
-    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityLabel();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> getAccessibilityRangeInfo();
-    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityValue();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getContentDescription();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getDisabled();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHidden();
@@ -2683,14 +2691,14 @@
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsPopup();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getSelected();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getStateDescription();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getTestTag();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> getText();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> getTextSelectionRange();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.state.ToggleableState> getToggleableState();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityScrollState> getVerticalAccessibilityScrollState();
-    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityLabel;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> AccessibilityRangeInfo;
-    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityValue;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> ContentDescription;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Disabled;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Hidden;
@@ -2699,6 +2707,7 @@
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsPopup;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Selected;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> StateDescription;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> TestTag;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> Text;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> TextSelectionRange;
@@ -2713,14 +2722,16 @@
     method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
-    method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
-    method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
-    method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method @Deprecated public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method @Deprecated public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static String getContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> getCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static boolean getFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.semantics.AccessibilityScrollState getHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.text.input.ImeAction getImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static boolean getSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static String getStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static String getTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.text.AnnotatedString getText(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void getTextLayoutResult(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean> action);
@@ -2733,9 +2744,9 @@
     method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> action);
-    method public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
-    method public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
-    method public static void setAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
+    method @Deprecated public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method @Deprecated public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method public static void setContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
     method public static void setCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> p);
     method public static void setFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
     method public static void setHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityScrollState p);
@@ -2743,6 +2754,8 @@
     method public static void setProgress(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> action);
     method public static void setSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
     method public static void setSelection(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Integer,? super java.lang.Boolean,java.lang.Boolean> action);
+    method public static void setStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method public static void setStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
     method public static void setTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
     method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.text.AnnotatedString p);
     method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean> action);
@@ -2833,11 +2846,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class AndroidDialogProperties implements androidx.compose.ui.window.DialogProperties {
-    ctor public AndroidDialogProperties(androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    ctor public AndroidDialogProperties(boolean dismissOnBackPress, boolean dismissOnClickOutside, androidx.compose.ui.window.SecureFlagPolicy securePolicy);
     ctor public AndroidDialogProperties();
-    method public androidx.compose.ui.window.SecureFlagPolicy component1();
-    method @androidx.compose.runtime.Immutable public androidx.compose.ui.window.AndroidDialogProperties copy(androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    method public boolean component1();
+    method public boolean component2();
+    method public androidx.compose.ui.window.SecureFlagPolicy component3();
+    method @androidx.compose.runtime.Immutable public androidx.compose.ui.window.AndroidDialogProperties copy(boolean dismissOnBackPress, boolean dismissOnClickOutside, androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    method public boolean getDismissOnBackPress();
+    method public boolean getDismissOnClickOutside();
     method public androidx.compose.ui.window.SecureFlagPolicy getSecurePolicy();
+    property public final boolean dismissOnBackPress;
+    property public final boolean dismissOnClickOutside;
     property public final androidx.compose.ui.window.SecureFlagPolicy securePolicy;
   }
 
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 62dd708..21ef65f 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -119,8 +119,7 @@
     method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
   }
 
-  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
-    method public void drawContent();
+  public final class ContentDrawScopeKt {
   }
 
   public final class DrawLayerModifierKt {
@@ -130,29 +129,32 @@
   public final class DrawModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
+  }
+
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
   }
 
   public final class FocusModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+    method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
+  @Deprecated public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+    method @Deprecated public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
     property public abstract kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange;
   }
 
   public final class FocusObserverModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
+    method @Deprecated public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+  public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
     method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
     property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
   }
 
   public final class FocusRequesterModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
+    method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
   }
 
   @androidx.compose.runtime.Stable public interface Modifier {
@@ -195,17 +197,17 @@
   public final class AndroidAutofillTypeKt {
   }
 
-  public interface Autofill {
+  @androidx.compose.ui.ExperimentalComposeUiApi public interface Autofill {
     method public void cancelAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
     method public void requestAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
   }
 
-  public final class AutofillNode {
+  @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillNode {
     ctor public AutofillNode(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
     method public java.util.List<androidx.compose.ui.autofill.AutofillType> component1();
     method public androidx.compose.ui.geometry.Rect? component2();
     method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? component3();
-    method public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
     method public java.util.List<androidx.compose.ui.autofill.AutofillType> getAutofillTypes();
     method public androidx.compose.ui.geometry.Rect? getBoundingBox();
     method public int getId();
@@ -217,7 +219,7 @@
     property public final kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? onFill;
   }
 
-  public final class AutofillTree {
+  @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillTree {
     ctor public AutofillTree();
     method public java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> getChildren();
     method public kotlin.Unit? performAutofill(int id, String value);
@@ -225,7 +227,7 @@
     property public final java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> children;
   }
 
-  public enum AutofillType {
+  @androidx.compose.ui.ExperimentalComposeUiApi public enum AutofillType {
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressAuxiliaryDetails;
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressCountry;
     enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressLocality;
@@ -279,7 +281,7 @@
     method public float getFontScale();
     method public long getSize-NH-jbRc();
     method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
     property public float density;
     property public float fontScale;
     property public final long size;
@@ -295,13 +297,13 @@
   }
 
   public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
-    method public void draw(androidx.compose.ui.ContentDrawScope);
+    method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
   }
 
   public final class DrawModifierKt {
     method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class DrawResult {
@@ -329,27 +331,61 @@
 
 package androidx.compose.ui.focus {
 
-  @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
+  public final class FocusChangedModifierKt {
+    method public static androidx.compose.ui.Modifier onFocusChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChanged);
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+  public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
+    method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+  }
+
+  public final class FocusEventModifierKt {
+    method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
+  }
+
+  public interface FocusManager {
     method public void clearFocus(optional boolean forcedClear);
   }
 
   public final class FocusNodeUtilsKt {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public final class FocusRequester {
+  public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
     method public boolean freeFocus();
     method public void requestFocus();
+    field public static final androidx.compose.ui.focus.FocusRequester.Companion Companion;
+  }
+
+  public static final class FocusRequester.Companion {
+    method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+  }
+
+  public static final class FocusRequester.Companion.FocusRequesterFactory {
+    method public operator androidx.compose.ui.focus.FocusRequester component1();
+    method public operator androidx.compose.ui.focus.FocusRequester component10();
+    method public operator androidx.compose.ui.focus.FocusRequester component11();
+    method public operator androidx.compose.ui.focus.FocusRequester component12();
+    method public operator androidx.compose.ui.focus.FocusRequester component13();
+    method public operator androidx.compose.ui.focus.FocusRequester component14();
+    method public operator androidx.compose.ui.focus.FocusRequester component15();
+    method public operator androidx.compose.ui.focus.FocusRequester component16();
+    method public operator androidx.compose.ui.focus.FocusRequester component2();
+    method public operator androidx.compose.ui.focus.FocusRequester component3();
+    method public operator androidx.compose.ui.focus.FocusRequester component4();
+    method public operator androidx.compose.ui.focus.FocusRequester component5();
+    method public operator androidx.compose.ui.focus.FocusRequester component6();
+    method public operator androidx.compose.ui.focus.FocusRequester component7();
+    method public operator androidx.compose.ui.focus.FocusRequester component8();
+    method public operator androidx.compose.ui.focus.FocusRequester component9();
+    field public static final androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory INSTANCE;
   }
 
   public final class FocusRequesterKt {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public enum FocusState {
+  public enum FocusState {
     enum_constant public static final androidx.compose.ui.focus.FocusState Active;
     enum_constant public static final androidx.compose.ui.focus.FocusState ActiveParent;
     enum_constant public static final androidx.compose.ui.focus.FocusState Captured;
@@ -411,9 +447,6 @@
     method public static androidx.compose.ui.Modifier dragSlopExceededGestureFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> onDragSlopExceeded, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean>? canDrag, optional androidx.compose.ui.gesture.scrollorientationlocking.Orientation? orientation);
   }
 
-  @kotlin.RequiresOptIn(message="This pointer input API is experimental and is likely to change before becoming " + "stable.") public @interface ExperimentalPointerInput {
-  }
-
   public final class GestureUtilsKt {
     method public static boolean anyPointersInBounds-5eFHUEc(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange>, long bounds);
   }
@@ -494,11 +527,11 @@
 
 package androidx.compose.ui.gesture.customevents {
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
+  public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
     ctor public DelayUpEvent(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
     method public androidx.compose.ui.gesture.customevents.DelayUpMessage component1();
     method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> component2();
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
+    method public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
     method public androidx.compose.ui.gesture.customevents.DelayUpMessage getMessage();
     method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> getPointers();
     method public void setMessage(androidx.compose.ui.gesture.customevents.DelayUpMessage p);
@@ -506,7 +539,7 @@
     property public final java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public enum DelayUpMessage {
+  public enum DelayUpMessage {
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayUp;
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpConsumed;
     enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpNotConsumed;
@@ -518,6 +551,37 @@
 
 }
 
+package androidx.compose.ui.gesture.nestedscroll {
+
+  public interface NestedScrollConnection {
+    method public default void onPostFling-Pv53iXo(long consumed, long available, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Velocity,kotlin.Unit> onFinished);
+    method public default long onPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+    method public default long onPreFling-TH1AsA0(long available);
+    method public default long onPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+  }
+
+  public final class NestedScrollDelegatingWrapperKt {
+  }
+
+  public final class NestedScrollDispatcher {
+    ctor public NestedScrollDispatcher();
+    method public void dispatchPostFling-uYzo7IE(long consumed, long available);
+    method public long dispatchPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+    method public long dispatchPreFling-TH1AsA0(long available);
+    method public long dispatchPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+  }
+
+  public final class NestedScrollModifierKt {
+    method public static androidx.compose.ui.Modifier nestedScroll(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection connection, optional androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher? dispatcher);
+  }
+
+  public enum NestedScrollSource {
+    enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Drag;
+    enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Fling;
+  }
+
+}
+
 package androidx.compose.ui.gesture.scrollorientationlocking {
 
   public enum Orientation {
@@ -525,7 +589,7 @@
     enum_constant public static final androidx.compose.ui.gesture.scrollorientationlocking.Orientation Vertical;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public final class ScrollOrientationLocker {
+  public final class ScrollOrientationLocker {
     ctor public ScrollOrientationLocker(androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher);
     method public void attemptToLockPointers(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
     method public java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> getPointersFor(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
@@ -679,7 +743,8 @@
 
   public final class VectorApplier extends androidx.compose.runtime.AbstractApplier<androidx.compose.ui.graphics.vector.VNode> {
     ctor public VectorApplier(androidx.compose.ui.graphics.vector.VNode root);
-    method public void insert(int index, androidx.compose.ui.graphics.vector.VNode instance);
+    method public void insertBottomUp(int index, androidx.compose.ui.graphics.vector.VNode instance);
+    method public void insertTopDown(int index, androidx.compose.ui.graphics.vector.VNode instance);
     method public void move(int from, int to, int count);
     method protected void onClear();
     method public void remove(int index, int count);
@@ -812,18 +877,6 @@
 
 package androidx.compose.ui.input.key {
 
-  @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
-    method @Deprecated public boolean isLeftAltPressed();
-    method @Deprecated public default boolean isPressed();
-    method @Deprecated public boolean isRightAltPressed();
-    property public abstract boolean isLeftAltPressed;
-    property public default boolean isPressed;
-    property public abstract boolean isRightAltPressed;
-  }
-
-  @kotlin.RequiresOptIn(message="The Key Input API is experimental and is likely to change in the future.") public @interface ExperimentalKeyInput {
-  }
-
   public final inline class Key {
     ctor public Key();
     method public static int constructor-impl(int keyCode);
@@ -1417,8 +1470,7 @@
     property public final int ZoomOut;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
-    method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
+  public interface KeyEvent {
     method public int getKey-EK5gGoQ();
     method public androidx.compose.ui.input.key.KeyEventType getType();
     method public int getUtf16CodePoint();
@@ -1426,7 +1478,6 @@
     method public boolean isCtrlPressed();
     method public boolean isMetaPressed();
     method public boolean isShiftPressed();
-    property @Deprecated public abstract androidx.compose.ui.input.key.Alt alt;
     property public abstract boolean isAltPressed;
     property public abstract boolean isCtrlPressed;
     property public abstract boolean isMetaPressed;
@@ -1436,15 +1487,17 @@
     property public abstract int utf16CodePoint;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public enum KeyEventType {
+  public enum KeyEventType {
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyDown;
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyUp;
     enum_constant public static final androidx.compose.ui.input.key.KeyEventType Unknown;
   }
 
   public final class KeyInputModifierKt {
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+    method @Deprecated public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+    method public static androidx.compose.ui.Modifier onKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+    method public static androidx.compose.ui.Modifier onPreviewKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+    method @Deprecated public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
   }
 
 }
@@ -1455,6 +1508,16 @@
     method public static boolean isMouseInput();
   }
 
+  @kotlin.coroutines.RestrictsSuspension public interface AwaitPointerEventScope extends androidx.compose.ui.unit.Density {
+    method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
+    method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
+    method public long getSize-YbymL2g();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
+    property public abstract long size;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
+  }
+
   public final class ConsumedData {
     method public boolean getDownChange();
     method public long getPositionChange-F1C5BW0();
@@ -1473,17 +1536,6 @@
     method public void retainHitPaths(java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointerIds);
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
-    method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
-    method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
-    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputData> getCurrentPointers();
-    method public long getSize-YbymL2g();
-    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    property public abstract java.util.List<androidx.compose.ui.input.pointer.PointerInputData> currentPointers;
-    property public abstract long size;
-    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
-  }
-
   public final class HitPathTrackerKt {
   }
 
@@ -1601,12 +1653,11 @@
     property public abstract androidx.compose.ui.input.pointer.PointerInputFilter pointerInputFilter;
   }
 
-  @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
-    method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
+  public interface PointerInputScope extends androidx.compose.ui.unit.Density {
+    method public suspend <R> Object? awaitPointerEventScope(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R> p);
     method public long getSize-YbymL2g();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
-    property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
+    method @Deprecated public default suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1627,7 +1678,7 @@
   }
 
   public final class SuspendingPointerInputFilterKt {
-    method @androidx.compose.ui.gesture.ExperimentalPointerInput public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+    method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
   }
 
 }
@@ -1786,15 +1837,31 @@
     property public abstract Object layoutId;
   }
 
+  public interface LayoutInfo {
+    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+    method public int getHeight();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
+    method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+    method public int getWidth();
+    method public boolean isAttached();
+    method public boolean isPlaced();
+    property public abstract androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public abstract int height;
+    property public abstract boolean isAttached;
+    property public abstract boolean isPlaced;
+    property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public abstract int width;
+  }
+
   public final class LayoutKt {
     method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.node.ExperimentalLayoutNodeApi public static inline void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocks, optional androidx.compose.ui.Modifier modifier);
-    method public static androidx.compose.ui.node.LayoutNode.MeasureBlocks MeasuringIntrinsicsMeasureBlocks(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
+    method @androidx.compose.runtime.Composable public static inline void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, androidx.compose.ui.node.MeasureBlocks measureBlocks, optional androidx.compose.ui.Modifier modifier);
+    method public static androidx.compose.ui.node.MeasureBlocks MeasuringIntrinsicsMeasureBlocks(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @Deprecated @androidx.compose.runtime.Composable public static void MultiMeasureLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function0<kotlin.Unit> children, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
     method @androidx.compose.runtime.Composable public static void WithConstraints(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.WithConstraintsScope,kotlin.Unit> content);
-    method @androidx.compose.ui.node.ExperimentalLayoutNodeApi @kotlin.PublishedApi internal static kotlin.jvm.functions.Function1<androidx.compose.runtime.SkippableUpdater<androidx.compose.ui.node.LayoutNode>,kotlin.Unit> materializerOf(androidx.compose.ui.Modifier modifier);
-    method @androidx.compose.ui.node.ExperimentalLayoutNodeApi public static androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocksOf(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
+    method @kotlin.PublishedApi internal static kotlin.jvm.functions.Function1<androidx.compose.runtime.SkippableUpdater<androidx.compose.ui.node.LayoutNode>,kotlin.Unit> materializerOf(androidx.compose.ui.Modifier modifier);
+    method public static androidx.compose.ui.node.MeasureBlocks measureBlocksOf(kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
   }
 
   public interface LayoutModifier extends androidx.compose.ui.Modifier.Element {
@@ -1842,6 +1909,16 @@
     method public static inline String! toString-impl(androidx.compose.ui.layout.Placeable! p);
   }
 
+  public final class ModifierInfo {
+    ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
+    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+    method public Object? getExtra();
+    method public androidx.compose.ui.Modifier getModifier();
+    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public final Object? extra;
+    property public final androidx.compose.ui.Modifier modifier;
+  }
+
   public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
     method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
   }
@@ -1947,6 +2024,9 @@
     method public java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class TestModifierUpdaterKt {
+  }
+
   public final class VerticalAlignmentLine extends androidx.compose.ui.layout.AlignmentLine {
     ctor public VerticalAlignmentLine(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> merger);
   }
@@ -1968,9 +2048,6 @@
 
 package androidx.compose.ui.node {
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level, message="This is an experimental API for Compose UI LayoutNode and is likely to change " + "before becoming stable.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalLayoutNodeApi {
-  }
-
   @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface InternalCoreApi {
   }
 
@@ -1978,43 +2055,26 @@
     method public kotlin.jvm.functions.Function0<androidx.compose.ui.node.LayoutNode> getConstructor();
     method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.unit.Density,kotlin.Unit> getSetDensity();
     method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.unit.LayoutDirection,kotlin.Unit> getSetLayoutDirection();
-    method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.node.LayoutNode.MeasureBlocks,kotlin.Unit> getSetMeasureBlocks();
+    method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.node.MeasureBlocks,kotlin.Unit> getSetMeasureBlocks();
     method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.Modifier,kotlin.Unit> getSetModifier();
     method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.node.Ref<androidx.compose.ui.node.LayoutNode>,kotlin.Unit> getSetRef();
     property public final kotlin.jvm.functions.Function0<androidx.compose.ui.node.LayoutNode> constructor;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.unit.Density,kotlin.Unit> setDensity;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.unit.LayoutDirection,kotlin.Unit> setLayoutDirection;
-    property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.node.LayoutNode.MeasureBlocks,kotlin.Unit> setMeasureBlocks;
+    property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.node.MeasureBlocks,kotlin.Unit> setMeasureBlocks;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.Modifier,kotlin.Unit> setModifier;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.node.Ref<androidx.compose.ui.node.LayoutNode>,kotlin.Unit> setRef;
   }
 
-  @androidx.compose.ui.node.ExperimentalLayoutNodeApi public final class LayoutNode implements androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
-    ctor public LayoutNode();
-    method public void attach(androidx.compose.ui.node.Owner owner);
-    method public void detach();
-    method public void draw(androidx.compose.ui.graphics.Canvas canvas);
+  public final class LayoutNode implements androidx.compose.ui.layout.LayoutInfo androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
     method public void forceRemeasure();
-    method public Integer? getAlignmentLine(androidx.compose.ui.layout.AlignmentLine line);
-    method @Deprecated public boolean getCanMultiMeasure();
-    method public java.util.List<androidx.compose.ui.node.LayoutNode> getChildren();
     method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
-    method public androidx.compose.ui.unit.Density getDensity();
-    method public int getDepth();
     method public int getHeight();
-    method public androidx.compose.ui.node.LayoutNode.MeasureBlocks getMeasureBlocks();
-    method public androidx.compose.ui.layout.MeasureScope getMeasureScope();
-    method public androidx.compose.ui.Modifier getModifier();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? getOnAttach();
-    method public kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? getOnDetach();
-    method public androidx.compose.ui.node.Owner? getOwner();
-    method public androidx.compose.ui.node.LayoutNode? getParent();
+    method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public Object? getParentData();
+    method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
     method public int getWidth();
-    method public void hitTest-fhABUH0(long pointerPositionRelativeToScreen, java.util.List<androidx.compose.ui.input.pointer.PointerInputFilter> hitPointerInputFilters);
-    method public void ignoreModelReads(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void insertAt(int index, androidx.compose.ui.node.LayoutNode instance);
+    method public boolean isAttached();
     method public boolean isPlaced();
     method public boolean isValid();
     method public int maxIntrinsicHeight(int width);
@@ -2022,39 +2082,20 @@
     method public androidx.compose.ui.layout.Placeable measure-BRTryo0(long constraints);
     method public int minIntrinsicHeight(int width);
     method public int minIntrinsicWidth(int height);
-    method public void move(int from, int to, int count);
-    method public void place(int x, int y);
-    method public void removeAll();
-    method public void removeAt(int index, int count);
-    method public void requestRelayout();
-    method public void requestRemeasure();
-    method @Deprecated public void setCanMultiMeasure(boolean p);
-    method public void setDensity(androidx.compose.ui.unit.Density p);
-    method public void setDepth(int p);
-    method public void setMeasureBlocks(androidx.compose.ui.node.LayoutNode.MeasureBlocks value);
-    method public void setModifier(androidx.compose.ui.Modifier value);
-    method public void setOnAttach(kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.Owner,kotlin.Unit>? p);
-    method public void setOnDetach(kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.Owner,kotlin.Unit>? p);
-    property @Deprecated public final boolean canMultiMeasure;
-    property public final java.util.List<androidx.compose.ui.node.LayoutNode> children;
-    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
-    property public final androidx.compose.ui.unit.Density density;
-    property public final int depth;
-    property public final int height;
-    property public final boolean isPlaced;
+    property public androidx.compose.ui.layout.LayoutCoordinates coordinates;
+    property public int height;
+    property public boolean isAttached;
+    property public boolean isPlaced;
     property public boolean isValid;
-    property public final androidx.compose.ui.node.LayoutNode.MeasureBlocks measureBlocks;
-    property public final androidx.compose.ui.layout.MeasureScope measureScope;
-    property public final androidx.compose.ui.Modifier modifier;
-    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? onAttach;
-    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.node.Owner,kotlin.Unit>? onDetach;
-    property public final androidx.compose.ui.node.Owner? owner;
-    property public final androidx.compose.ui.node.LayoutNode? parent;
     property public Object? parentData;
-    property public final int width;
+    property public androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public int width;
   }
 
-  public static interface LayoutNode.MeasureBlocks {
+  public final class LayoutNodeKt {
+  }
+
+  public interface MeasureBlocks {
     method public int maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
     method public int maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
     method public androidx.compose.ui.layout.MeasureResult measure-8A2P9vY(androidx.compose.ui.layout.MeasureScope measureScope, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, long constraints);
@@ -2062,28 +2103,6 @@
     method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
   }
 
-  public abstract static class LayoutNode.NoIntrinsicsMeasureBlocks implements androidx.compose.ui.node.LayoutNode.MeasureBlocks {
-    ctor public LayoutNode.NoIntrinsicsMeasureBlocks(String error);
-    method public Void maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
-    method public Void maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
-    method public Void minIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int w);
-    method public Void minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
-  }
-
-  public final class LayoutNodeKt {
-    method public static androidx.compose.ui.node.LayoutNode? findClosestParentNode(androidx.compose.ui.node.LayoutNode, kotlin.jvm.functions.Function1<? super androidx.compose.ui.node.LayoutNode,java.lang.Boolean> selector);
-  }
-
-  public final class ModifierInfo {
-    ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
-    method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
-    method public Object? getExtra();
-    method public androidx.compose.ui.Modifier getModifier();
-    property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
-    property public final Object? extra;
-    property public final androidx.compose.ui.Modifier modifier;
-  }
-
   public interface OwnedLayer {
     method public void destroy();
     method public void drawLayer(androidx.compose.ui.graphics.Canvas canvas);
@@ -2107,7 +2126,6 @@
     method public androidx.compose.ui.focus.FocusManager getFocusManager();
     method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
     method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
-    method public boolean getHasPendingMeasureOrLayout();
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
     method public long getMeasureIteration();
     method public androidx.compose.ui.node.LayoutNode getRoot();
@@ -2125,7 +2143,7 @@
     method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
     method public void onSemanticsChange();
     method public boolean requestFocus();
-    method @androidx.compose.ui.input.key.ExperimentalKeyInput public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
+    method public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
     property public abstract androidx.compose.ui.autofill.Autofill? autofill;
     property public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
     property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
@@ -2133,7 +2151,6 @@
     property public abstract androidx.compose.ui.focus.FocusManager focusManager;
     property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
     property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
-    property public abstract boolean hasPendingMeasureOrLayout;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
     property public abstract long measureIteration;
     property public abstract androidx.compose.ui.node.LayoutNode root;
@@ -2169,16 +2186,13 @@
     property public final T? value;
   }
 
-  public final class UiApplier implements androidx.compose.runtime.Applier<java.lang.Object> {
+  public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
     ctor public UiApplier(Object root);
-    method public void clear();
-    method public void down(Object node);
-    method public Object getCurrent();
-    method public void insert(int index, Object instance);
+    method public void insertBottomUp(int index, Object instance);
+    method public void insertTopDown(int index, Object instance);
     method public void move(int from, int to, int count);
+    method protected void onClear();
     method public void remove(int index, int count);
-    method public void up();
-    property public Object current;
   }
 
   public final class ViewInteropKt {
@@ -2216,8 +2230,6 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
-    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
-    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.focus.FocusManager>! getFocusManagerAmbient();
@@ -2249,31 +2261,6 @@
   public final class AndroidComposeViewKt {
   }
 
-  public interface AndroidOwner extends androidx.compose.ui.node.Owner {
-    method @androidx.compose.ui.node.ExperimentalLayoutNodeApi public void addAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, androidx.compose.ui.node.LayoutNode layoutNode);
-    method public void drawAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, android.graphics.Canvas canvas);
-    method public kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> getConfigurationChangeObserver();
-    method public android.view.View getView();
-    method public androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? getViewTreeOwners();
-    method public void invalidateDescendants();
-    method public void removeAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view);
-    method public void setConfigurationChangeObserver(kotlin.jvm.functions.Function1<? super android.content.res.Configuration,kotlin.Unit> p);
-    method public void setOnViewTreeOwnersAvailable(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners,kotlin.Unit> callback);
-    property public abstract kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> configurationChangeObserver;
-    property public abstract android.view.View view;
-    property public abstract androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? viewTreeOwners;
-  }
-
-  public static final class AndroidOwner.ViewTreeOwners {
-    ctor public AndroidOwner.ViewTreeOwners(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner);
-    method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
-    method public androidx.savedstate.SavedStateRegistryOwner getSavedStateRegistryOwner();
-    method public androidx.lifecycle.ViewModelStoreOwner getViewModelStoreOwner();
-    property public final androidx.lifecycle.LifecycleOwner lifecycleOwner;
-    property public final androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner;
-    property public final androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner;
-  }
-
   public final class AndroidUriHandler implements androidx.compose.ui.platform.UriHandler {
     ctor public AndroidUriHandler(android.content.Context context);
     method public void openUri(String uri);
@@ -2352,10 +2339,6 @@
   public final class JvmActualsKt {
   }
 
-  public final class SubcompositionKt {
-    method @MainThread public static androidx.compose.runtime.Composition subcomposeInto(androidx.compose.ui.node.LayoutNode container, androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
-  }
-
   public final class TestTagKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier testTag(androidx.compose.ui.Modifier, String tag);
   }
@@ -2407,6 +2390,23 @@
     property public abstract float touchSlop;
   }
 
+  @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+    method public boolean getHasPendingMeasureOrLayout();
+    method public android.view.View getView();
+    method public void invalidateDescendants();
+    method public boolean isLifecycleInResumedState();
+    property public abstract boolean hasPendingMeasureOrLayout;
+    property public abstract boolean isLifecycleInResumedState;
+    property public abstract android.view.View view;
+    field public static final androidx.compose.ui.platform.ViewRootForTest.Companion Companion;
+  }
+
+  public static final class ViewRootForTest.Companion {
+    method public kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? getOnViewCreatedCallback();
+    method public void setOnViewCreatedCallback(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? p);
+    property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
+  }
+
   @androidx.compose.runtime.Stable public interface WindowManager {
     method public boolean isWindowFocused();
     property public abstract boolean isWindowFocused;
@@ -2464,6 +2464,10 @@
     ctor public LoadedResource(T? resource);
   }
 
+  public final class PainterResourcesKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+  }
+
   public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
     ctor public PendingResource(T? resource);
   }
@@ -2498,7 +2502,7 @@
 
 package androidx.compose.ui.selection {
 
-  public interface Selectable {
+  @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
     method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
     method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
     method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2548,9 +2552,11 @@
   public final class SelectionManagerKt {
   }
 
-  public interface SelectionRegistrar {
-    method public void onPositionChange();
-    method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+  @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+    method public void notifyPositionChange();
+    method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+    method public void notifySelectionUpdateEnd();
+    method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
     method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
     method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
   }
@@ -2664,12 +2670,13 @@
     method public operator <T> T! get(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key);
     method public <T> T! getOrElse(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
     method public <T> T? getOrElseNullable(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
-    method public boolean isEmpty();
+    method public boolean isClearingSemantics();
     method public boolean isMergingSemanticsOfDescendants();
     method public java.util.Iterator<java.util.Map.Entry<androidx.compose.ui.semantics.SemanticsPropertyKey<?>,java.lang.Object>> iterator();
     method public <T> void set(androidx.compose.ui.semantics.SemanticsPropertyKey<T> key, T? value);
+    method public void setClearingSemantics(boolean p);
     method public void setMergingSemanticsOfDescendants(boolean p);
-    property public final boolean isEmpty;
+    property public final boolean isClearingSemantics;
     property public final boolean isMergingSemanticsOfDescendants;
   }
 
@@ -2685,6 +2692,7 @@
   }
 
   public final class SemanticsModifierKt {
+    method public static androidx.compose.ui.Modifier clearAndSetSemantics(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> properties);
     method public static androidx.compose.ui.Modifier semantics(androidx.compose.ui.Modifier, optional boolean mergeDescendants, kotlin.jvm.functions.Function1<? super androidx.compose.ui.semantics.SemanticsPropertyReceiver,kotlin.Unit> properties);
   }
 
@@ -2692,25 +2700,27 @@
     method public int getAlignmentLinePosition(androidx.compose.ui.layout.AlignmentLine line);
     method public androidx.compose.ui.geometry.Rect getBoundsInRoot();
     method public java.util.List<androidx.compose.ui.semantics.SemanticsNode> getChildren();
-    method public androidx.compose.ui.node.LayoutNode getComponentNode();
     method public androidx.compose.ui.semantics.SemanticsConfiguration getConfig();
     method public androidx.compose.ui.geometry.Rect getGlobalBounds();
     method public long getGlobalPosition-F1C5BW0();
     method public int getId();
+    method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
     method public boolean getMergingEnabled();
+    method public androidx.compose.ui.node.Owner? getOwner();
     method public androidx.compose.ui.semantics.SemanticsNode? getParent();
     method public long getPositionInRoot-F1C5BW0();
     method public long getSize-YbymL2g();
     method public boolean isRoot();
     property public final androidx.compose.ui.geometry.Rect boundsInRoot;
     property public final java.util.List<androidx.compose.ui.semantics.SemanticsNode> children;
-    property public final androidx.compose.ui.node.LayoutNode componentNode;
     property public final androidx.compose.ui.semantics.SemanticsConfiguration config;
     property public final androidx.compose.ui.geometry.Rect globalBounds;
     property public final long globalPosition;
     property public final int id;
     property public final boolean isRoot;
+    property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
     property public final boolean mergingEnabled;
+    property public final androidx.compose.ui.node.Owner? owner;
     property public final androidx.compose.ui.semantics.SemanticsNode? parent;
     property public final long positionInRoot;
     property public final long size;
@@ -2720,7 +2730,6 @@
   }
 
   public final class SemanticsOwner {
-    ctor public SemanticsOwner(androidx.compose.ui.node.LayoutNode rootNode);
     method public androidx.compose.ui.node.LayoutNode getRootNode();
     method public androidx.compose.ui.semantics.SemanticsNode getRootSemanticsNode();
     method public androidx.compose.ui.semantics.SemanticsNode getUnmergedRootSemanticsNode();
@@ -2734,9 +2743,8 @@
   }
 
   public final class SemanticsProperties {
-    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityLabel();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> getAccessibilityRangeInfo();
-    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityValue();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getContentDescription();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getDisabled();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHidden();
@@ -2745,14 +2753,14 @@
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsPopup();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getSelected();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getStateDescription();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getTestTag();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> getText();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> getTextSelectionRange();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.state.ToggleableState> getToggleableState();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityScrollState> getVerticalAccessibilityScrollState();
-    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityLabel;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> AccessibilityRangeInfo;
-    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityValue;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> ContentDescription;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Disabled;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Hidden;
@@ -2761,6 +2769,7 @@
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsPopup;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Selected;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> StateDescription;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> TestTag;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> Text;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> TextSelectionRange;
@@ -2775,14 +2784,16 @@
     method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
-    method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
-    method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
-    method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method @Deprecated public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method @Deprecated public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static String getContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> getCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static boolean getFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.semantics.AccessibilityScrollState getHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.text.input.ImeAction getImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static boolean getSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static String getStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static String getTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.text.AnnotatedString getText(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void getTextLayoutResult(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean> action);
@@ -2795,9 +2806,9 @@
     method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> action);
-    method public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
-    method public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
-    method public static void setAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
+    method @Deprecated public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method @Deprecated public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method public static void setContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
     method public static void setCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> p);
     method public static void setFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
     method public static void setHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityScrollState p);
@@ -2805,6 +2816,8 @@
     method public static void setProgress(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> action);
     method public static void setSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
     method public static void setSelection(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Integer,? super java.lang.Boolean,java.lang.Boolean> action);
+    method public static void setStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+    method public static void setStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
     method public static void setTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
     method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.text.AnnotatedString p);
     method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean> action);
@@ -2895,11 +2908,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class AndroidDialogProperties implements androidx.compose.ui.window.DialogProperties {
-    ctor public AndroidDialogProperties(androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    ctor public AndroidDialogProperties(boolean dismissOnBackPress, boolean dismissOnClickOutside, androidx.compose.ui.window.SecureFlagPolicy securePolicy);
     ctor public AndroidDialogProperties();
-    method public androidx.compose.ui.window.SecureFlagPolicy component1();
-    method @androidx.compose.runtime.Immutable public androidx.compose.ui.window.AndroidDialogProperties copy(androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    method public boolean component1();
+    method public boolean component2();
+    method public androidx.compose.ui.window.SecureFlagPolicy component3();
+    method @androidx.compose.runtime.Immutable public androidx.compose.ui.window.AndroidDialogProperties copy(boolean dismissOnBackPress, boolean dismissOnClickOutside, androidx.compose.ui.window.SecureFlagPolicy securePolicy);
+    method public boolean getDismissOnBackPress();
+    method public boolean getDismissOnClickOutside();
     method public androidx.compose.ui.window.SecureFlagPolicy getSecurePolicy();
+    property public final boolean dismissOnBackPress;
+    property public final boolean dismissOnClickOutside;
     property public final androidx.compose.ui.window.SecureFlagPolicy securePolicy;
   }
 
diff --git a/compose/ui/ui/integration-tests/ui-demos/build.gradle b/compose/ui/ui/integration-tests/ui-demos/build.gradle
index eb67fb8..9a5536b 100644
--- a/compose/ui/ui/integration-tests/ui-demos/build.gradle
+++ b/compose/ui/ui/integration-tests/ui-demos/build.gradle
@@ -18,6 +18,7 @@
     implementation project(":compose:foundation:foundation")
     implementation project(":compose:foundation:foundation-layout")
     implementation project(":compose:integration-tests:demos:common")
+    implementation project(":compose:ui:ui:ui-samples")
     implementation project(":compose:material:material")
     implementation project(":compose:runtime:runtime")
     implementation project(":compose:runtime:runtime-livedata")
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 021cf64..5f74c0c 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -45,6 +45,7 @@
 import androidx.compose.integration.demos.common.DemoCategory
 import androidx.compose.ui.demos.focus.FocusInDialog
 import androidx.compose.ui.demos.focus.FocusInPopup
+import androidx.compose.ui.samples.NestedScrollSample
 
 private val GestureDemos = DemoCategory(
     "Gestures",
@@ -87,7 +88,8 @@
                 ComposableDemo("Nested Long Press") { NestedLongPressDemo() },
                 ComposableDemo("Pointer Input During Sub Comp") { PointerInputDuringSubComp() }
             )
-        )
+        ),
+        ComposableDemo("New nested scroll") { NestedScrollSample() }
     )
 )
 
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt
index d225bf9..8465693 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/VectorGraphicsDemo.kt
@@ -24,12 +24,11 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.HorizontalGradient
-import androidx.compose.ui.graphics.RadialGradient
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.TileMode
-import androidx.compose.ui.graphics.VerticalGradient
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.graphics.vector.Group
 import androidx.compose.ui.graphics.vector.Path
@@ -104,7 +103,7 @@
                     close()
                 }
                 Path(
-                    fill = HorizontalGradient(
+                    fill = Brush.horizontalGradient(
                         listOf(
                             Color.Red,
                             Color.Blue
@@ -130,12 +129,13 @@
     }
 
     Path(
-        fill = VerticalGradient(
+        fill = Brush.verticalGradient(
             0.0f to Color.Cyan,
             0.3f to Color.Green,
             1.0f to Color.Magenta,
             startY = 0.0f,
-            endY = vectorHeight
+            endY = vectorHeight,
+            tileMode = TileMode.Clamp
         ),
         pathData = background
     )
@@ -145,14 +145,13 @@
 private fun Triangle() {
     val length = 150.0f
     Path(
-        fill = RadialGradient(
+        fill = Brush.radialGradient(
             listOf(
                 Color(0xFF000080),
                 Color(0xFF808000),
                 Color(0xFF008080)
             ),
-            centerX = length / 2.0f,
-            centerY = length / 2.0f,
+            Offset(length / 2.0f, length / 2.0f),
             radius = length / 2.0f,
             tileMode = TileMode.Repeated
         ),
@@ -170,13 +169,13 @@
     val side1 = 150.0f
     val side2 = 150.0f
     Path(
-        fill = RadialGradient(
+        fill = Brush.radialGradient(
             0.0f to Color(0xFF800000),
             0.3f to Color.Cyan,
             0.8f to Color.Yellow,
-            centerX = side1 / 2.0f,
-            centerY = side2 / 2.0f,
-            radius = side1 / 2.0f
+            center = Offset(side1 / 2.0f, side2 / 2.0f),
+            radius = side1 / 2.0f,
+            tileMode = TileMode.Clamp
         ),
         pathData = PathData {
             horizontalLineToRelative(side1)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
index 9a32990..6ae1e6e 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
@@ -29,12 +29,12 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.autofill.AutofillNode
 import androidx.compose.ui.autofill.AutofillType
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.layout.LayoutCoordinates
@@ -46,10 +46,7 @@
 import androidx.compose.ui.unit.dp
 
 @Composable
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
 fun ExplicitAutofillTypesDemo() {
     Column {
         val nameState = remember { mutableStateOf("Enter name here") }
@@ -64,7 +61,7 @@
             onFill = { nameState.value = it }
         ) { autofillNode ->
             BasicTextField(
-                modifier = Modifier.focusObserver {
+                modifier = Modifier.onFocusChanged {
                     autofill?.apply {
                         if (it.isFocused) {
                             requestAutofillForNode(autofillNode)
@@ -91,7 +88,7 @@
             onFill = { emailState.value = it }
         ) { autofillNode ->
             BasicTextField(
-                modifier = Modifier.focusObserver {
+                modifier = Modifier.onFocusChanged {
                     autofill?.run {
                         if (it.isFocused) {
                             requestAutofillForNode(autofillNode)
@@ -112,6 +109,7 @@
     }
 }
 
+@ExperimentalComposeUiApi
 @Composable
 private fun Autofill(
     autofillTypes: List<AutofillType>,
@@ -139,4 +137,4 @@
         x.toInt() + size.width,
         y.toInt() + size.height
     )
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt
index efbbc4e..bda62a94 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt
@@ -28,10 +28,9 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.graphics.Color.Companion.Black
@@ -58,14 +57,13 @@
 }
 
 @Composable
-@OptIn(ExperimentalFocus::class)
 private fun FocusableText(text: String) {
     var color by remember { mutableStateOf(Black) }
     val focusRequester = FocusRequester()
     Text(
         modifier = Modifier
             .focusRequester(focusRequester)
-            .focusObserver { color = if (it.isFocused) Green else Black }
+            .onFocusChanged { color = if (it.isFocused) Green else Black }
             .focus()
             .tapGestureFilter { focusRequester.requestFocus() },
         text = text,
@@ -80,4 +78,4 @@
         horizontalArrangement = Arrangement.Center,
         content = content
     )
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt
index f0998a0..6918b8e 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt
@@ -30,10 +30,9 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
@@ -44,7 +43,6 @@
 private enum class CurrentShape { Circle, Square }
 
 @Composable
-@OptIn(ExperimentalFocus::class)
 fun ReuseFocusRequester() {
     Column(
         verticalArrangement = Arrangement.Top
@@ -76,7 +74,6 @@
 }
 
 @Composable
-@OptIn(ExperimentalFocus::class)
 private fun Circle(modifier: Modifier = Modifier, nextShape: () -> Unit) {
     var isFocused by remember { mutableStateOf(false) }
     val scale = animate(if (isFocused) 0f else 1f, TweenSpec(2000)) {
@@ -87,7 +84,7 @@
     val radius = size / 2
     Canvas(
         modifier
-            .focusObserver { isFocused = it.isFocused }
+            .onFocusChanged { isFocused = it.isFocused }
             .fillMaxSize()
             .focus()
     ) {
@@ -100,7 +97,6 @@
 }
 
 @Composable
-@OptIn(ExperimentalFocus::class)
 private fun Square(modifier: Modifier = Modifier, nextShape: () -> Unit) {
     var isFocused by remember { mutableStateOf(false) }
     val scale = animate(if (isFocused) 0f else 1f, TweenSpec(2000)) {
@@ -111,7 +107,7 @@
     val side = size
     Canvas(
         modifier
-            .focusObserver { isFocused = it.isFocused }
+            .onFocusChanged { isFocused = it.isFocused }
             .fillMaxSize()
             .focus()
     ) {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
index fd1f866..ba2d319 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
@@ -27,7 +27,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt
index f6ea061..8901b03 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt
@@ -23,7 +23,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
@@ -60,27 +60,28 @@
                 "it is actually a new item that has not been hit tested yet.  If you keep " +
                 "your finger there and then add more fingers, it will track those new fingers."
         )
-        LazyColumnFor(
-            List(100) { index -> index },
+        LazyColumn(
             Modifier
                 .fillMaxSize()
                 .wrapContentSize(Alignment.Center)
                 .size(200.dp)
                 .background(color = Color.White)
         ) {
-            val pointerCount = remember { mutableStateOf(0) }
+            items(List(100) { index -> index }) {
+                val pointerCount = remember { mutableStateOf(0) }
 
-            Box(
-                Modifier.fillParentMaxSize()
-                    .border(width = 1.dp, color = Color.Black)
-                    .pointerCounterGestureFilter { newCount -> pointerCount.value = newCount },
-                contentAlignment = Alignment.Center
-            ) {
-                Text(
-                    "${pointerCount.value}",
-                    fontSize = TextUnit.Em(16),
-                    color = Color.Black
-                )
+                Box(
+                    Modifier.fillParentMaxSize()
+                        .border(width = 1.dp, color = Color.Black)
+                        .pointerCounterGestureFilter { newCount -> pointerCount.value = newCount },
+                    contentAlignment = Alignment.Center
+                ) {
+                    Text(
+                        "${pointerCount.value}",
+                        fontSize = TextUnit.Em(16),
+                        color = Color.Black
+                    )
+                }
             }
         }
     }
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
index 1b6c95a..b888fa7 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
@@ -34,7 +34,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
@@ -52,6 +52,7 @@
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
 
@@ -115,7 +116,7 @@
             Modifier
                 .fillMaxHeight()
                 .width(drawerWidth)
-                .offset(x = { currentOffset.value })
+                .offset { IntOffset(currentOffset.value.roundToInt(), 0) }
                 .background(color = DefaultBackgroundColor)
         ) {
             Text(
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt
index 4fe780d..4750324 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt
@@ -29,16 +29,14 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
-import androidx.compose.ui.focusObserver
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEventType.KeyDown
-import androidx.compose.ui.input.key.keyInputFilter
+import androidx.compose.ui.input.key.onKeyEvent
 
 @Composable
 fun KeyInputDemo() {
@@ -63,20 +61,16 @@
 }
 
 @Composable
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class
-)
 private fun FocusableText(text: MutableState<String>) {
     var color by remember { mutableStateOf(Color.Black) }
     val focusRequester = FocusRequester()
     Text(
         modifier = Modifier
             .focusRequester(focusRequester)
-            .focusObserver { color = if (it.isFocused) Color.Green else Color.Black }
+            .onFocusChanged { color = if (it.isFocused) Color.Green else Color.Black }
             .focus()
             .tapGestureFilter { focusRequester.requestFocus() }
-            .keyInputFilter {
+            .onKeyEvent {
                 if (it.type == KeyDown) {
                     text.value = StringBuilder(text.value)
                         .appendCodePoint(it.utf16CodePoint)
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
index cbae372..5500315 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
@@ -22,14 +22,23 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
 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.draw.drawBehind
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.platform.AmbientContext
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import kotlin.math.roundToInt
 
 @Suppress("SetTextI18n")
 @Sampled
@@ -43,3 +52,20 @@
         view.layoutParams = ViewGroup.LayoutParams(size, size)
     }
 }
+
+@Sampled
+@Composable
+fun AndroidDrawableInDrawScopeSample() {
+    val drawable = ContextCompat.getDrawable(AmbientContext.current, R.drawable.sample_drawable)
+    Box(
+        modifier = Modifier.size(100.dp)
+            .drawBehind {
+                drawIntoCanvas { canvas ->
+                    drawable?.let {
+                        it.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
+                        it.draw(canvas.nativeCanvas)
+                    }
+                }
+            }
+    )
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
index 04a0ab1..15c1dbd 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
@@ -26,12 +26,13 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.LinearGradient
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.vector.Path
 import androidx.compose.ui.graphics.vector.PathData
@@ -50,12 +51,10 @@
 fun DrawWithCacheModifierSample() {
     Box(
         Modifier.drawWithCache {
-            val gradient = LinearGradient(
-                startX = 0.0f,
-                startY = 0.0f,
-                endX = size.width,
-                endY = size.height,
-                colors = listOf(Color.Red, Color.Blue)
+            val gradient = Brush.linearGradient(
+                colors = listOf(Color.Red, Color.Blue),
+                start = Offset.Zero,
+                end = Offset(size.width, size.height)
             )
             onDrawBehind {
                 drawRect(gradient)
@@ -66,9 +65,9 @@
 
 /**
  * Sample showing how to leverage [Modifier.drawWithCache] to persist data across
- * draw calls. In the example below, the [LinearGradient] will be re-created if either
+ * draw calls. In the example below, the linear gradient will be re-created if either
  * the size of the drawing area changes, or the toggle flag represented by a mutable state
- * object changes. Otherwise the same [LinearGradient] instance is re-used for each call
+ * object changes. Otherwise the same linear gradient instance is re-used for each call
  * to drawRect.
  */
 @Sampled
@@ -79,12 +78,10 @@
     var toggle by remember { mutableStateOf(true) }
     Box(
         Modifier.clickable { toggle = !toggle }.drawWithCache {
-            val gradient = LinearGradient(
-                startX = 0.0f,
-                startY = 0.0f,
-                endX = size.width,
-                endY = size.height,
-                colors = if (toggle) colors1 else colors2
+            val gradient = Brush.linearGradient(
+                colors = if (toggle) colors1 else colors2,
+                start = Offset.Zero,
+                end = Offset(size.width, size.height)
             )
             onDrawBehind {
                 drawRect(gradient)
@@ -115,12 +112,10 @@
     Image(
         painter = vectorPainter,
         modifier = Modifier.size(120.dp).drawWithCache {
-            val gradient = LinearGradient(
+            val gradient = Brush.linearGradient(
                 colors = listOf(Color.Red, Color.Blue),
-                startX = 0f,
-                startY = 0f,
-                endX = 0f,
-                endY = size.height
+                start = Offset.Zero,
+                end = Offset(0f, size.height)
             )
             onDrawWithContent {
                 drawContent()
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
index 42e5856..9beccc6 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
@@ -27,7 +27,6 @@
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.layout.measureBlocksOf
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.offset
 
@@ -79,7 +78,6 @@
 
 @Sampled
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class)
 fun LayoutWithMeasureBlocksWithIntrinsicUsage(content: @Composable () -> Unit) {
     val measureBlocks = measureBlocksOf(
         minIntrinsicWidthMeasureBlock = { measurables, h ->
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt
new file mode 100644
index 0000000..4a95ba4
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.ScrollCallback
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
+import androidx.compose.ui.gesture.scrollGestureFilter
+import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.minus
+import kotlin.math.roundToInt
+
+@Sampled
+@Composable
+fun NestedScrollSample() {
+    // constructing the box with next that scrolls as long as text within 0 .. 300
+    // to support nested scrolling, we need to scroll ourselves, dispatch nested scroll events
+    // as we scroll, and listen to potential children when we're scrolling.
+    val maxValue = 300f
+    val minValue = 0f
+    // our state that we update as scroll
+    var value by remember { mutableStateOf(maxValue / 2) }
+    // create dispatch to dispatch scroll events up to the nested scroll parents
+    val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
+    // we're going to scroll vertically, so set the orientation to vertical
+    val orientation = Orientation.Vertical
+
+    // callback to listen to scroll events and dispatch nested scroll events
+    val scrollCallback = remember {
+        object : ScrollCallback {
+            override fun onScroll(scrollDistance: Float): Float {
+                // dispatch prescroll with Y axis since we're going vertical scroll
+                val aboveConsumed = nestedScrollDispatcher.dispatchPreScroll(
+                    Offset(x = 0f, y = scrollDistance),
+                    NestedScrollSource.Drag
+                )
+                // adjust what we can consume according to pre-scroll
+                val available = scrollDistance - aboveConsumed.y
+                // let's calculate how much we want to consume and how much is left
+                val newTotal = value + available
+                val newValue = newTotal.coerceIn(minValue, maxValue)
+                val toConsume = newValue - value
+                val leftAfterUs = available - toConsume
+                // consume ourselves what we need and dispatch "scroll" phase of nested scroll
+                value += toConsume
+                nestedScrollDispatcher.dispatchPostScroll(
+                    Offset(x = 0f, y = toConsume),
+                    Offset(x = 0f, y = leftAfterUs),
+                    NestedScrollSource.Drag
+                )
+                // indicate to the old pointer that we handled everything by returning same value
+                return scrollDistance
+            }
+
+            override fun onStop(velocity: Float) {
+                // for simplicity we won't fling ourselves, but we need to respect nested scroll
+                // dispatch pre fling
+                val velocity2d = Velocity(Offset(x = 0f, y = velocity))
+                val consumed = nestedScrollDispatcher.dispatchPreFling(velocity2d)
+                // now, since we don't fling, we consume 0 (Offset.Zero).
+                // Adjust what's left after prefling and dispatch post fling
+                val left = velocity2d - consumed
+                nestedScrollDispatcher.dispatchPostFling(Velocity.Zero, left)
+            }
+        }
+    }
+
+    // we also want to participate in the nested scrolling, not only dispatching. create connection
+    val connection = remember {
+        object : NestedScrollConnection {
+            // let's assume we want to consume children's delta before them if we can
+            // we should do it in pre scroll
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                // calculate how much we can take from child
+                val oldValue = value
+                val newTotal = value + available.y
+                val newValue = newTotal.coerceIn(minValue, maxValue)
+                val toConsume = newValue - oldValue
+                // consume what we want and report back co children can adjust
+                value += toConsume
+                return Offset(x = 0f, y = toConsume)
+            }
+        }
+    }
+
+    // scrollable parent to which we will dispatch our nested scroll events
+    // Since we properly support scrolling above, this parent will scroll even if we scroll inner
+    // box (with White background)
+    LazyColumn(Modifier.background(Color.Red)) {
+        // our box we constructed
+        item {
+            Box(
+                Modifier
+                    .size(width = 300.dp, height = 100.dp)
+                    .background(Color.White)
+                    // add scrolling listening and dispatching
+                    .scrollGestureFilter(orientation = orientation, scrollCallback = scrollCallback)
+                    // connect self connection and dispatcher to the nested scrolling system
+                    .nestedScroll(connection, dispatcher = nestedScrollDispatcher)
+            ) {
+                // hypothetical scrollable child which we will listen in connection above
+                LazyColumn {
+                    items(listOf(1, 2, 3, 4, 5)) {
+                        Text(
+                            "Magenta text above will change first when you scroll me",
+                            modifier = Modifier.padding(5.dp)
+                        )
+                    }
+                }
+                // simply show our value. It will change when we scroll child list above, taking
+                // child's scroll delta until we reach maxValue or minValue
+                Text(
+                    text = value.roundToInt().toString(),
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .background(Color.Magenta)
+                )
+            }
+        }
+        repeat(100) {
+            item {
+                Text(
+                    "Outer scroll items are Yellow on Red parent",
+                    modifier = Modifier.background(Color.Yellow).padding(5.dp)
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
index ef4205e..dfce282 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
@@ -17,9 +17,11 @@
 package androidx.compose.ui.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.paint
@@ -27,6 +29,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
 
 @Sampled
@@ -53,4 +56,16 @@
                 .background(color = Color.Yellow)
                 .paint(CustomPainter())
     ) { /** intentionally empty **/ }
+}
+
+@Sampled
+@Composable
+fun PainterResourceSample() {
+    // Sample showing how to render a Painter based on a different resource (vector vs png)
+    // Here a Vector asset is used in the portrait orientation, however, a png is used instead
+    // in the landscape orientation based on the res/drawable and res/drawable-land-hdpi folders
+    Image(
+        painterResource(R.drawable.ic_vector_or_png),
+        modifier = Modifier.size(50.dp)
+    )
 }
\ No newline at end of file
diff --git a/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png b/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png
new file mode 100755
index 0000000..4696f99
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png
Binary files differ
diff --git a/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml b/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml
new file mode 100644
index 0000000..64452ad
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml
@@ -0,0 +1,36 @@
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector android:height="200dp" android:viewportHeight="144"
+    android:viewportWidth="144" android:width="200dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#F79D80" android:pathData="M69.26,55.73m-53.39,0a53.39,53.39 0,1 1,106.78 0a53.39,53.39 0,1 1,-106.78 0"/>
+    <path android:fillColor="#37474F" android:pathData="M47.66,76.35h2.26v65.31h-2.26z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M61.47,22.88l7.59,0.63C69.06,23.51 65.07,22.59 61.47,22.88z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M78.86,44.71c-6.8,-5.5 -22.66,-7.48 -22.99,-17.32c-0.11,-3.17 2.61,-4.26 5.6,-4.5l-11.02,-0.92c-3.05,9.16 1.58,18.08 14.69,24.18c10.96,5.1 6.86,12.67 3.64,16.47c-3.13,-3.32 -7.57,-5.4 -12.49,-5.4c-0.78,0 -1.54,0.06 -2.29,0.16c-6.08,0.37 -39.94,5.23 -35.41,67.19c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.88,60.43 86.05,50.53 78.86,44.71z"/>
+    <path android:fillColor="#434343" android:pathData="M68.8,62.41c-3.13,-3.32 -7.57,-5.4 -12.49,-5.4c-0.78,0 -1.54,0.06 -2.29,0.16c-6.08,0.37 -39.94,5.23 -35.41,67.19c0,0 1.34,-0.96 3.61,-2.58c-2.04,-56.41 29.85,-60.98 35.73,-61.34c0.75,-0.1 1.51,-0.16 2.29,-0.16C65.16,60.29 66.58,61.23 68.8,62.41"/>
+    <path android:fillColor="#FFD54F" android:pathData="M83.23,22.46l20.76,1.43c0.4,0.03 0.52,-0.52 0.16,-0.67l-19.1,-7.78c-0.2,-0.08 -0.42,0.03 -0.47,0.24l-1.66,6.35C82.86,22.23 83.01,22.44 83.23,22.46z"/>
+    <path android:fillColor="#F9BF2C" android:pathData="M86.66,16.09l-1.62,-0.66c-0.2,-0.08 -0.42,0.03 -0.47,0.24l-1.66,6.36c-0.06,0.21 0.1,0.42 0.31,0.44l10.52,0.72L86.66,16.09z"/>
+    <path android:fillColor="#1B2428" android:pathData="M47.66,103.79l0,3.86l2.24,2.24l0,-7.7z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M71.76,7.67c-5.16,-0.35 -9.83,0.75 -12.82,2.74l0,0c-7.5,4.2 -9.01,13.44 -9.18,14.36c-0.18,0.93 3.05,2.01 3.05,2.01l4.12,-4.16l4.52,-0.06c2.52,1.27 5.61,2.55 9.09,2.79c8.62,0.59 15.88,-2.89 16.22,-7.77C87.1,12.7 80.38,8.26 71.76,7.67z"/>
+    <path android:fillColor="#881A51" android:pathData="M71.76,7.67c-5.16,-0.35 -9.83,0.75 -12.82,2.74l0,0c-7.5,4.2 -9.01,13.44 -9.18,14.36c-0.18,0.93 3.05,2.01 3.05,2.01l4.12,-4.16l4.67,-0.38c2.52,1.27 7.02,1.03 10.5,1.27c8.62,0.59 14.33,-1.05 14.66,-5.93C87.1,12.7 80.38,8.26 71.76,7.67z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M71.83,11.32c-5.16,-0.29 -9.83,0.61 -12.82,2.21l0,0c-7.5,3.39 -9.16,10.46 -9.25,11.23c-0.15,1.18 3.05,3.13 3.05,3.13l4.2,-4.48l4.47,-0.54c2.52,1.03 5.59,2.28 9.07,2.47c8.62,0.48 15.08,-2.54 15.41,-6.49C86.29,14.92 80.45,11.8 71.83,11.32z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M73.53,24.78c-3.48,-0.19 -6.55,-1.44 -9.07,-2.47c0,0 0,0 0,0l-1.08,0.13c-0.68,0.11 -1.33,0.28 -1.91,0.52l0.11,-0.01c0,0 0,0 0,0c2.52,1.03 5.59,2.28 9.07,2.47c3.37,0.19 6.4,-0.16 8.87,-0.89C77.7,24.8 75.68,24.9 73.53,24.78z"/>
+    <path android:fillColor="#EEEEEE" android:pathData="M82.13,48.13c1.75,4.57 2.05,9.63 0.33,14.43c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.33,0.24 -41.01,29.29 -53.34,38.1c0.03,2.98 0.16,6.12 0.41,9.41c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.13,62.01 86.99,53.98 82.13,48.13z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M78.86,44.91c-0.16,-0.13 -0.33,-0.26 -0.5,-0.39c5.68,5.93 8.39,14.63 5.53,22.58c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.07,0.05 -43.58,31.16 -54.59,39.02c0.06,1.4 0.15,2.8 0.26,4.26c0,0 57.03,-40.74 57.11,-40.79c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.88,60.64 86.05,50.73 78.86,44.91z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M78.94,44.76c-6.8,-5.5 -22.66,-7.48 -22.99,-17.32c-0.04,-1.21 0.33,-2.11 0.97,-2.78c-1.81,0.6 -3.12,1.8 -3.04,4.02C54.2,38.52 70.07,40.5 76.86,46c7.19,5.82 11.02,15.73 7.81,24.68c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.07,0.05 -44.69,31.92 -54.89,39.21c0.01,0.08 0.01,0.16 0.02,0.24c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.96,60.49 86.13,50.59 78.94,44.76z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M54.5,28.8c0,0 -0.52,0.61 -0.52,0.61c0.01,-0.01 -0.02,-0.11 -0.02,-0.12c-0.02,-0.11 -0.03,-0.23 -0.05,-0.34c-0.04,-0.4 -0.06,-0.79 -0.04,-1.19c0.02,-0.55 0.1,-1.1 0.24,-1.64c0.17,-0.63 0.43,-1.24 0.78,-1.79c0.42,-0.65 0.96,-1.21 1.58,-1.66c0.32,-0.23 0.66,-0.43 1.01,-0.61c0.47,-0.24 0.93,-0.43 1.46,-0.49c0.27,-0.03 0.55,-0.05 0.82,-0.05c0.89,-0.02 1.78,0.07 2.65,0.24c0.37,0.07 0.73,0.16 1.09,0.26c0.19,0.05 0.39,0.11 0.58,0.17c0.1,0.03 0.2,0.07 0.3,0.1c0.04,0.01 0.27,0.13 0.3,0.11c0,0 -1.79,1.07 -1.79,1.07s-5.78,-1.34 -6.91,3.25L54.5,28.8z"/>
+</vector>
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml
similarity index 68%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml
index 3bc2684..68053bc 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml
@@ -14,6 +14,11 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.car.app">
-</manifest>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+  <gradient
+      android:startColor="@android:color/holo_red_dark"
+      android:centerColor="@android:color/holo_orange_dark"
+      android:endColor="@android:color/holo_blue_dark" />
+</shape>
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 6219052..8fb5451 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -38,7 +38,6 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
 import androidx.compose.ui.platform.testTag
@@ -59,7 +58,6 @@
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.input.TextFieldValue
-import androidx.core.os.BuildCompat
 import androidx.core.view.ViewCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -92,7 +90,6 @@
 @RunWith(AndroidJUnit4::class)
 @OptIn(
     ExperimentalFoundationApi::class,
-    ExperimentalLayoutNodeApi::class
 )
 class AndroidAccessibilityTest {
     @get:Rule
@@ -176,7 +173,7 @@
         var accessibilityNodeInfo = provider.createAccessibilityNodeInfo(toggleableNode.id)
         assertEquals("android.view.View", accessibilityNodeInfo.className)
         val stateDescription = when {
-            BuildCompat.isAtLeastR() -> {
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
                 accessibilityNodeInfo.stateDescription
             }
             Build.VERSION.SDK_INT >= 19 -> {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 0b258d2..0802cb8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -23,7 +23,6 @@
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.FrameLayout
 import androidx.activity.ComponentActivity
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.InnerPlaceable
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.platform.AmbientClipboardManager
@@ -35,8 +34,8 @@
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.SemanticsWrapper
-import androidx.compose.ui.semantics.accessibilityValue
-import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.stateDescriptionRange
 import androidx.compose.ui.semantics.dismiss
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.getTextLayoutResult
@@ -51,7 +50,6 @@
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextRange
-import androidx.core.os.BuildCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -73,7 +71,6 @@
 import org.mockito.ArgumentMatcher
 import org.mockito.ArgumentMatchers
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class AndroidComposeViewAccessibilityDelegateCompatTest {
@@ -113,9 +110,9 @@
         val info = AccessibilityNodeInfoCompat.obtain()
         val clickActionLabel = "click"
         val dismissActionLabel = "dismiss"
-        val accessibilityValue = "checked"
-        val semanticsModifier = SemanticsModifierCore(1, true) {
-            this.accessibilityValue = accessibilityValue
+        val stateDescription = "checked"
+        val semanticsModifier = SemanticsModifierCore(1, true, false) {
+            this.stateDescription = stateDescription
             onClick(clickActionLabel) { true }
             dismiss(dismissActionLabel) { true }
         }
@@ -143,8 +140,8 @@
                 )
             )
         )
-        val stateDescription = when {
-            BuildCompat.isAtLeastR() -> {
+        val stateDescriptionResult = when {
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
                 info.unwrap().stateDescription
             }
             Build.VERSION.SDK_INT >= 19 -> {
@@ -156,7 +153,7 @@
                 null
             }
         }
-        assertEquals(accessibilityValue, stateDescription)
+        assertEquals(stateDescription, stateDescriptionResult)
         assertTrue(info.isClickable)
         assertTrue(info.isVisibleToUser)
     }
@@ -165,8 +162,8 @@
     fun testPopulateAccessibilityNodeInfoProperties_SeekBar() {
         val info = AccessibilityNodeInfoCompat.obtain()
         val setProgressActionLabel = "setProgress"
-        val semanticsModifier = SemanticsModifierCore(1, true) {
-            accessibilityValueRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
+        val semanticsModifier = SemanticsModifierCore(1, true, false) {
+            stateDescriptionRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
             setProgress(setProgressActionLabel) { true }
         }
         val semanticsNode = SemanticsNode(
@@ -201,7 +198,7 @@
         val setSelectionActionLabel = "setSelection"
         val setTextActionLabel = "setText"
         val text = "hello"
-        val semanticsModifier = SemanticsModifierCore(1, true) {
+        val semanticsModifier = SemanticsModifierCore(1, true, false) {
             this.text = AnnotatedString(text)
             this.textSelectionRange = TextRange(1)
             this.focused = true
@@ -389,7 +386,7 @@
         mergeDescendants: Boolean,
         properties: (SemanticsPropertyReceiver.() -> Unit)
     ): SemanticsNode {
-        val semanticsModifier = SemanticsModifierCore(id, mergeDescendants, properties)
+        val semanticsModifier = SemanticsModifierCore(id, mergeDescendants, false, properties)
         return SemanticsNode(
             SemanticsWrapper(InnerPlaceable(LayoutNode()), semanticsModifier),
             true
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index bffcb8c8..46ca6ca 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -59,6 +59,7 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.drawscope.clipRect
 import androidx.compose.ui.graphics.drawscope.translate
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
index b234a6d..f596611 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.dispatch.AndroidUiDispatcher
 import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.createAndroidComposeBenchmarkRunner
@@ -29,6 +30,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.yield
@@ -51,6 +53,9 @@
         class SimpleTestCase : ComposeTestCase {
             @Composable
             override fun Content() {
+                // The following line adds coverage for delayed coroutine memory leaks.
+                LaunchedEffect(Unit) { delay(10000) }
+
                 Column {
                     repeat(3) {
                         Box {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
index f5aa880..8119ae9 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
@@ -22,6 +22,7 @@
 import android.view.autofill.AutofillValue
 import androidx.autofill.HintConstants.AUTOFILL_HINT_PERSON_NAME
 import androidx.compose.testutils.fake.FakeViewStructure
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.platform.AmbientAutofill
 import androidx.compose.ui.platform.AmbientAutofillTree
@@ -36,6 +37,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalComposeUiApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class AndroidAutoFillTest {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
index 4dfe5bb..81f6c55 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
@@ -19,7 +19,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.FocusModifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -30,7 +29,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class CaptureFocusTest {
     @get:Rule
@@ -44,7 +42,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(FocusState.Active))
             )
@@ -70,7 +68,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -96,7 +94,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -122,7 +120,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -148,7 +146,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
index 382c8fa..238100b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
@@ -32,7 +32,6 @@
 import org.junit.runners.Parameterized
 
 @SmallTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(Parameterized::class)
 class ClearFocusTest(val forcedClear: Boolean) {
     @get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
index ff6adc0..121fc03 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
@@ -30,7 +30,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FindFocusableChildrenTest {
     @get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
index 24f8ee1..7db84d4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
@@ -30,7 +30,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FindParentFocusNodeTest {
     @get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedCountTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedCountTest.kt
new file mode 100644
index 0000000..61c69b6
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedCountTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus
+import androidx.compose.ui.focus.FocusState.Inactive
+import androidx.compose.ui.focus.FocusState.Active
+import androidx.compose.ui.focusRequester
+import androidx.compose.ui.platform.AmbientFocusManager
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class FocusChangedCountTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun initially_focusChangedIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
+
+    @Test
+    fun initiallyNoFocusModifier_onFocusChangedIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        rule.setFocusableContent {
+            Box(modifier = Modifier.onFocusChanged { focusStates.add(it) })
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
+
+    @Test
+    fun whenFocusIsGained_focusChangedIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Active) }
+    }
+
+    @Test
+    fun requestingFocusWhenAlreadyFocused_onFocusChangedIsNotCalledAgain() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            focusStates.clear()
+        }
+
+        // Act.
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).isEmpty() }
+    }
+
+    @Test
+    fun whenFocusIsLost_focusChangedIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        lateinit var focusManager: FocusManager
+        rule.setFocusableContent {
+            focusManager = AmbientFocusManager.current
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            focusStates.clear()
+        }
+
+        // Act.
+        rule.runOnIdle { focusManager.clearFocus() }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
+
+    @Test
+    fun removingActiveFocusNode_onFocusChangedIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        lateinit var addFocusModifier: MutableState<Boolean>
+        rule.setFocusableContent {
+            addFocusModifier = remember { mutableStateOf(true) }
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            focusStates.clear()
+        }
+
+        // Act.
+        rule.runOnIdle { addFocusModifier.value = false }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
+
+    @Test
+    fun removingInactiveFocusNode_onFocusChangedIsNotCalled() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        lateinit var addFocusModifier: MutableState<Boolean>
+        rule.setFocusableContent {
+            addFocusModifier = remember { mutableStateOf(true) }
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusStates.add(it) }
+                    .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { addFocusModifier.value = false }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).isEmpty() }
+    }
+
+    @Test
+    fun addingFocusModifier_onFocusChangedIsNotCalled() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        lateinit var addFocusModifier: MutableState<Boolean>
+        rule.setFocusableContent {
+            addFocusModifier = remember { mutableStateOf(false) }
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { focusStates.add(it) }
+                    .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { addFocusModifier.value = true }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).isEmpty() }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusObserverTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
similarity index 82%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusObserverTest.kt
rename to compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
index dee66f9..f1b75a0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusObserverTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
@@ -25,7 +25,6 @@
 import androidx.compose.ui.focus.FocusState.Captured
 import androidx.compose.ui.focus.FocusState.Disabled
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -36,9 +35,8 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
-class FocusObserverTest {
+class FocusChangedTest {
     @get:Rule
     val rule = createComposeRule()
 
@@ -50,7 +48,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(Active))
             )
@@ -69,12 +67,11 @@
     fun activeParent_requestFocus() {
         // Arrange.
         lateinit var focusState: FocusState
-        val focusRequester = FocusRequester()
-        val childFocusRequester = FocusRequester()
+        val (focusRequester, childFocusRequester) = FocusRequester.createRefs()
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .focus()
             ) {
@@ -107,7 +104,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(Captured))
             )
@@ -130,7 +127,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(Disabled))
             )
@@ -153,7 +150,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(Inactive))
             )
@@ -181,19 +178,19 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState1 = it }
+                    .onFocusChanged { focusState2 = it }
             ) {
                 Box {
                     Box(
                         modifier = Modifier
-                            .focusObserver { focusState3 = it }
-                            .focusObserver { focusState4 = it }
+                            .onFocusChanged { focusState3 = it }
+                            .onFocusChanged { focusState4 = it }
                     ) {
                         Box(
                             modifier = Modifier
-                                .focusObserver { focusState5 = it }
-                                .focusObserver { focusState6 = it }
+                                .onFocusChanged { focusState5 = it }
+                                .onFocusChanged { focusState6 = it }
                                 .focusRequester(focusRequester)
                                 .then(FocusModifier(Inactive))
                         )
@@ -227,32 +224,25 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState1 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focus()
-                    .focusObserver { focusState3 = it }
-                    .focusObserver { focusState4 = it }
+                    .onFocusChanged { focusState3 = it }
+                    .onFocusChanged { focusState4 = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
         }
-        rule.runOnIdle {
-            focusRequester.requestFocus()
-            focusState1 = null
-            focusState2 = null
-            focusState3 = null
-            focusState4 = null
-        }
 
         rule.runOnIdle {
             // Act.
             focusRequester.requestFocus()
 
             // Assert.
-            assertThat(focusState1).isNull()
-            assertThat(focusState2).isNull()
+            assertThat(focusState1).isEqualTo(ActiveParent)
+            assertThat(focusState2).isEqualTo(ActiveParent)
             assertThat(focusState3).isEqualTo(Active)
             assertThat(focusState4).isEqualTo(Active)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
new file mode 100644
index 0000000..57ecc0a
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus
+import androidx.compose.ui.focus.FocusState.Inactive
+import androidx.compose.ui.focus.FocusState.Active
+import androidx.compose.ui.focusRequester
+import androidx.compose.ui.platform.AmbientFocusManager
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class FocusEventCountTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun initially_onFocusEventIsCalledThrice() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusStates).containsExactly(
+                Inactive, // triggered by onFocusEvent node's onModifierChanged().
+                Inactive, // triggered by focus node's onModifierChanged().
+                Inactive, // triggered by focus node's attach().
+            )
+        }
+    }
+
+    @Test
+    fun initiallyNoFocusModifier_onFocusEventIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        rule.setFocusableContent {
+            Box(modifier = Modifier.onFocusEvent { focusStates.add(it) })
+        }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
+
+    @Test
+    fun whenFocusIsGained_onFocusEventIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Active) }
+    }
+
+    @Test
+    fun requestingFocusWhenAlreadyFocused_onFocusEventIsCalledAgain() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            focusStates.clear()
+        }
+
+        // Act.
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Active) }
+    }
+
+    @Test
+    fun whenFocusIsLost_onFocusEventIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        lateinit var focusManager: FocusManager
+        rule.setFocusableContent {
+            focusManager = AmbientFocusManager.current
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .focus()
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            focusStates.clear()
+        }
+
+        // Act.
+        rule.runOnIdle { focusManager.clearFocus() }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
+
+    @Test
+    fun removingActiveFocusNode_onFocusEventIsCalledTwice() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        lateinit var addFocusModifier: MutableState<Boolean>
+        rule.setFocusableContent {
+            addFocusModifier = remember { mutableStateOf(true) }
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .focusRequester(focusRequester)
+                    .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            focusStates.clear()
+        }
+
+        // Act.
+        rule.runOnIdle { addFocusModifier.value = false }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusStates).containsExactly(
+                Inactive, // triggered by focus node's state change.
+                Inactive, // triggered by onFocusEvent node's onModifierChanged().
+            )
+        }
+    }
+
+    @Test
+    fun removingInactiveFocusNode_onFocusEventIsCalledOnce() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        lateinit var addFocusModifier: MutableState<Boolean>
+        rule.setFocusableContent {
+            addFocusModifier = remember { mutableStateOf(true) }
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { addFocusModifier.value = false }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).containsExactly(Inactive) }
+    }
+
+    @Test
+    fun addingFocusModifier_onFocusEventIsCalledThrice() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        lateinit var addFocusModifier: MutableState<Boolean>
+        rule.setFocusableContent {
+            addFocusModifier = remember { mutableStateOf(false) }
+            Box(
+                modifier = Modifier
+                    .onFocusEvent { focusStates.add(it) }
+                    .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
+            )
+        }
+        rule.runOnIdle { focusStates.clear() }
+
+        // Act.
+        rule.runOnIdle { addFocusModifier.value = true }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusStates).containsExactly(
+                Inactive, // triggered by focus node's attach().
+                Inactive, // triggered by onFocusEvent node's onModifierChanged().
+                Inactive, // triggered by focus node's onModifierChanged().
+            )
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt
index ccf7260..2155298 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.ActiveParent
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.platform.AmbientFocusManager
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -34,7 +33,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FocusManagerAmbientTest {
     @get:Rule
@@ -52,7 +50,7 @@
             Box(
                 modifier = Modifier
                     .focusRequester(focusRequester)
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focus()
             )
         }
@@ -81,18 +79,18 @@
             focusRequester = FocusRequester()
             Box(
                 modifier = Modifier
-                    .focusObserver { grandparentFocusState = it }
+                    .onFocusChanged { grandparentFocusState = it }
                     .focus()
             ) {
                 Box(
                     modifier = Modifier
-                        .focusObserver { parentFocusState = it }
+                        .onFocusChanged { parentFocusState = it }
                         .focus()
                 ) {
                     Box(
                         modifier = Modifier
                             .focusRequester(focusRequester)
-                            .focusObserver { focusState = it }
+                            .onFocusChanged { focusState = it }
                             .focus()
                     )
                 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt
index b8f96cb..07046b5c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.focus.FocusState.ActiveParent
 import androidx.compose.ui.focus.FocusState.Captured
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -37,34 +36,33 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FocusModifierAttachDetachTest {
     @get:Rule
     val rule = createComposeRule()
 
     @Test
-    fun reorderedFocusRequesterModifiers_focusObserverInSameModifierChain() {
+    fun reorderedFocusRequesterModifiers_onFocusChangedInSameModifierChain() {
         // Arrange.
         var focusState = Inactive
         val focusRequester = FocusRequester()
         lateinit var observingFocusModifier1: MutableState<Boolean>
         rule.setFocusableContent {
             val focusRequesterModifier = Modifier.focusRequester(focusRequester)
-            val focusObserver = Modifier.focusObserver { focusState = it }
+            val onFocusChanged = Modifier.onFocusChanged { focusState = it }
             val focusModifier1 = Modifier.focus()
             val focusModifier2 = Modifier.focus()
             Box {
                 observingFocusModifier1 = remember { mutableStateOf(true) }
                 Box(
                     modifier = if (observingFocusModifier1.value) {
-                        focusObserver
+                        onFocusChanged
                             .then(focusRequesterModifier)
                             .then(focusModifier1)
                             .then(focusModifier2)
                     } else {
                         focusModifier1
-                            .then(focusObserver)
+                            .then(onFocusChanged)
                             .then(focusRequesterModifier)
                             .then(focusModifier2)
                     }
@@ -84,25 +82,25 @@
     }
 
     @Test
-    fun removedModifier_focusObserverDoesNotHaveAFocusModifier() {
+    fun removedModifier_onFocusChangedDoesNotHaveAFocusModifier() {
         // Arrange.
         var focusState = Inactive
         val focusRequester = FocusRequester()
-        lateinit var focusObserverHasFocusModifier: MutableState<Boolean>
+        lateinit var onFocusChangedHasFocusModifier: MutableState<Boolean>
         rule.setFocusableContent {
             val focusRequesterModifier = Modifier.focusRequester(focusRequester)
-            val focusObserver = Modifier.focusObserver { focusState = it }
+            val onFocusChanged = Modifier.onFocusChanged { focusState = it }
             val focusModifier = Modifier.focus()
             Box {
-                focusObserverHasFocusModifier = remember { mutableStateOf(true) }
+                onFocusChangedHasFocusModifier = remember { mutableStateOf(true) }
                 Box(
-                    modifier = if (focusObserverHasFocusModifier.value) {
-                        focusObserver
+                    modifier = if (onFocusChangedHasFocusModifier.value) {
+                        onFocusChanged
                             .then(focusRequesterModifier)
                             .then(focusModifier)
                     } else {
                         focusModifier
-                            .then(focusObserver)
+                            .then(onFocusChanged)
                             .then(focusRequesterModifier)
                     }
                 )
@@ -114,7 +112,7 @@
         }
 
         // Act.
-        rule.runOnIdle { focusObserverHasFocusModifier.value = false }
+        rule.runOnIdle { onFocusChangedHasFocusModifier.value = false }
 
         // Assert.
         rule.runOnIdle { assertThat(focusState).isEqualTo(Inactive) }
@@ -129,7 +127,7 @@
         rule.setFocusableContent {
             optionalFocusModifier = remember { mutableStateOf(true) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(if (optionalFocusModifier.value) Modifier.focus() else Modifier)
             )
@@ -155,7 +153,7 @@
         rule.setFocusableContent {
             optionalFocusModifier = remember { mutableStateOf(true) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(if (optionalFocusModifier.value) Modifier.focus() else Modifier)
             ) {
@@ -183,7 +181,7 @@
         rule.setFocusableContent {
             optionalFocusModifier = remember { mutableStateOf(true) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(if (optionalFocusModifier.value) Modifier.focus() else Modifier)
             ) {
@@ -212,7 +210,7 @@
         rule.setFocusableContent {
             optionalFocusModifier = remember { mutableStateOf(true) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .then(if (optionalFocusModifier.value) Modifier.focus() else Modifier)
                     .focusRequester(focusRequester)
             ) {
@@ -240,7 +238,7 @@
         rule.setFocusableContent {
             optionalFocusModifier = remember { mutableStateOf(true) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .then(
                         if (optionalFocusModifier.value) {
                             Modifier
@@ -276,11 +274,11 @@
             optionalFocusModifiers = remember { mutableStateOf(true) }
             Box(
                 modifier = Modifier
-                    .focusObserver { parentFocusState = it }
+                    .onFocusChanged { parentFocusState = it }
                     .focus()
             ) {
                 Box(
-                    modifier = Modifier.focusObserver { focusState = it }.then(
+                    modifier = Modifier.onFocusChanged { focusState = it }.then(
                         if (optionalFocusModifiers.value) {
                             Modifier.focus()
                                 .focusRequester(focusRequester)
@@ -317,7 +315,7 @@
         rule.setFocusableContent {
             optionalFocusModifier = remember { mutableStateOf(true) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .then(if (optionalFocusModifier.value) Modifier.focus() else Modifier)
                     .focusRequester(focusRequester)
                     .focus()
@@ -340,7 +338,7 @@
         rule.setFocusableContent {
             addFocusModifier = remember { mutableStateOf(false) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
             ) {
@@ -368,7 +366,7 @@
         rule.setFocusableContent {
             addFocusModifier = remember { mutableStateOf(false) }
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(if (addFocusModifier.value) Modifier.focus() else Modifier)
             )
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
index 3f04cfa..45873ee 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.ui.focus
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.platform.AmbientView
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -35,7 +34,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FocusRequesterTest {
     @get:Rule
@@ -49,7 +47,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
             )
         }
@@ -71,7 +69,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focus()
                     .focusRequester(focusRequester)
             )
@@ -94,7 +92,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
@@ -118,7 +116,7 @@
             Box(
                 modifier = Modifier
                     .focusRequester(focusRequester)
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
             ) {
                 Box(modifier = Modifier.focus())
             }
@@ -140,7 +138,7 @@
         val focusRequester = FocusRequester()
         rule.setFocusableContent {
             Box(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
             ) {
                 Box(
                     modifier = Modifier
@@ -170,7 +168,7 @@
             ) {
                 Box(
                     modifier = Modifier
-                        .focusObserver { focusState = it }
+                        .onFocusChanged { focusState = it }
                         .focus()
                 )
             }
@@ -193,7 +191,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
             ) {
                 Box {
@@ -233,12 +231,12 @@
             ) {
                 Box(
                     modifier = Modifier
-                        .focusObserver { focusState1 = it }
+                        .onFocusChanged { focusState1 = it }
                         .focus()
                 )
                 Box(
                     modifier = Modifier
-                        .focusObserver { focusState2 = it }
+                        .onFocusChanged { focusState2 = it }
                         .focus()
                 )
             }
@@ -255,16 +253,15 @@
     }
 
     @Test
-    fun requestFocusForAnyChild_triggersFocusObserverInParent() {
+    fun requestFocusForAnyChild_triggersonFocusChangedInParent() {
         // Arrange.
         lateinit var hostView: View
         var focusState = Inactive
-        val focusRequester1 = FocusRequester()
-        val focusRequester2 = FocusRequester()
+        val (focusRequester1, focusRequester2) = FocusRequester.createRefs()
         rule.setFocusableContent {
             hostView = AmbientView.current
             Column(
-                modifier = Modifier.focusObserver { focusState = it }
+                modifier = Modifier.onFocusChanged { focusState = it }
             ) {
                 Box(
                     modifier = Modifier
@@ -305,4 +302,4 @@
             assertThat(focusState).isEqualTo(Active)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
index bd92c83..5481a25 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
@@ -24,7 +24,6 @@
 import androidx.compose.ui.focus.FocusState.Captured
 import androidx.compose.ui.focus.FocusState.Disabled
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +34,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FreeFocusTest {
     @get:Rule
@@ -49,7 +47,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -73,7 +71,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -97,7 +95,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -121,7 +119,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -145,7 +143,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -160,4 +158,4 @@
             Truth.assertThat(focusState).isEqualTo(Inactive)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
index 88a6397..2b31db7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.ui.focus
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.platform.AmbientView
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -36,7 +35,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class OwnerFocusTest {
     @get:Rule
@@ -78,7 +76,7 @@
             ownerView = getOwner()
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
@@ -106,7 +104,7 @@
             ownerView = getOwner()
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
@@ -133,7 +131,7 @@
             ownerView = getOwner()
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
@@ -163,7 +161,7 @@
             ownerView = getOwner()
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
index 8f92466..a7c6af1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
@@ -32,7 +32,6 @@
 import org.junit.runners.Parameterized
 
 @SmallTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(Parameterized::class)
 class RequestFocusTest(val propagateFocus: Boolean) {
     @get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
index 6c3630e..c7e108e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Captured
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -33,7 +32,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class ReusedFocusRequesterCaptureFocusTest {
     @get:Rule
@@ -47,7 +45,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -71,7 +69,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -95,7 +93,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -120,13 +118,13 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState1))
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState2))
             )
@@ -152,13 +150,13 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState1))
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState2))
             )
@@ -184,13 +182,13 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState1))
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState2))
             )
@@ -206,4 +204,4 @@
             assertThat(focusState2).isEqualTo(Inactive)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
index 8c652a1..125bfb8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Captured
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -33,7 +32,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class ReusedFocusRequesterFreeFocusTest {
     @get:Rule
@@ -47,7 +45,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -71,7 +69,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -95,7 +93,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState))
             )
@@ -120,13 +118,13 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState1))
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState2))
             )
@@ -152,13 +150,13 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState1))
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState2))
             )
@@ -184,13 +182,13 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState1))
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .then(FocusModifier(focusState2))
             )
@@ -206,4 +204,4 @@
             assertThat(focusState2).isEqualTo(Inactive)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
index 0993a57..804474a 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.focus
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -32,7 +31,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class ReusedFocusRequesterTest {
     @get:Rule
@@ -46,7 +44,7 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState = it }
+                    .onFocusChanged { focusState = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
@@ -70,13 +68,13 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
@@ -102,19 +100,19 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState1 = it }
+                    .onFocusChanged { focusState1 = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState2 = it }
+                    .onFocusChanged { focusState2 = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
             Box(
                 modifier = Modifier
-                    .focusObserver { focusState3 = it }
+                    .onFocusChanged { focusState3 = it }
                     .focusRequester(focusRequester)
                     .focus()
             )
@@ -130,4 +128,4 @@
             assertThat(focusState3).isEqualTo(Active)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt
index d6a6633..dd50fc4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focusObserver
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -36,7 +35,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class SetRootFocusTest {
     @get:Rule
@@ -62,7 +60,7 @@
                             focusRequester.requestFocus()
                         }
                         .focusRequester(focusRequester)
-                        .focusObserver { isFocused = it.isFocused }
+                        .onFocusChanged { isFocused = it.isFocused }
                         .focus()
                 )
                 BasicText(
@@ -80,4 +78,4 @@
         // Assert.
         rule.runOnIdle { assertThat(isFocused).isFalse() }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifierTest.kt
new file mode 100644
index 0000000..513b0f6
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifierTest.kt
@@ -0,0 +1,1062 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.gesture.nestedscroll
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.minus
+import androidx.compose.ui.unit.plus
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class NestedScrollModifierTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val preScrollOffset = Offset(120f, 120f)
+    private val scrollOffset = Offset(125f, 125f)
+    private val scrollLeftOffset = Offset(32f, 32f)
+    private val preFling = Velocity(Offset(120f, 120f))
+    private val postFlingConsumed = Velocity(Offset(151f, 63f))
+    private val postFlingLeft = Velocity(Offset(11f, 13f))
+
+    @Test
+    fun nestedScroll_twoNodes_orderTest() {
+        var counter = 0
+        val childConnection = object : NestedScrollConnection {}
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(counter).isEqualTo(1)
+                counter++
+                return Offset.Zero
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(counter).isEqualTo(3)
+                counter++
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(counter).isEqualTo(5)
+                counter++
+                return Velocity.Zero
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(counter).isEqualTo(7)
+                counter++
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                Box(
+                    Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(0)
+            counter++
+
+            childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(counter).isEqualTo(2)
+            counter++
+
+            childDispatcher
+                .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Drag)
+            assertThat(counter).isEqualTo(4)
+            counter++
+
+            childDispatcher.dispatchPreFling(preFling)
+            assertThat(counter).isEqualTo(6)
+            counter++
+
+            childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+            assertThat(counter).isEqualTo(8)
+            counter++
+        }
+    }
+
+    @Test
+    fun nestedScroll_NNodes_orderTest_preScroll() {
+        var counter = 0
+        val childConnection = object : NestedScrollConnection {}
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(counter).isEqualTo(2)
+                counter++
+                return Offset.Zero
+            }
+        }
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(counter).isEqualTo(1)
+                counter++
+                return Offset.Zero
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(0)
+            counter++
+
+            childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(counter).isEqualTo(3)
+            counter++
+        }
+    }
+
+    @Test
+    fun nestedScroll_NNodes_orderTest_scroll() {
+        var counter = 0
+        val childConnection = object : NestedScrollConnection {}
+        val parentConnection = object : NestedScrollConnection {
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(counter).isEqualTo(1)
+                counter++
+                return Offset.Zero
+            }
+        }
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(counter).isEqualTo(2)
+                counter++
+                return Offset.Zero
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(0)
+            counter++
+
+            childDispatcher
+                .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Drag)
+            assertThat(counter).isEqualTo(3)
+            counter++
+        }
+    }
+
+    @Test
+    fun nestedScroll_NNodes_orderTest_preFling() {
+        var counter = 0
+        val childConnection = object : NestedScrollConnection {}
+        val parentConnection = object : NestedScrollConnection {
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(counter).isEqualTo(2)
+                counter++
+                return Velocity.Zero
+            }
+        }
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(counter).isEqualTo(1)
+                counter++
+                return Velocity.Zero
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(0)
+            counter++
+
+            childDispatcher.dispatchPreFling(preFling)
+            assertThat(counter).isEqualTo(3)
+            counter++
+        }
+    }
+
+    @Test
+    fun nestedScroll_NNodes_orderTest_fling() {
+        var counter = 0
+        val childConnection = object : NestedScrollConnection {}
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(counter).isEqualTo(1)
+                counter++
+                onFinished.invoke(Velocity.Zero)
+            }
+        }
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(counter).isEqualTo(2)
+                counter++
+                onFinished.invoke(Velocity.Zero)
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(counter).isEqualTo(0)
+            counter++
+
+            childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+
+            assertThat(counter).isEqualTo(3)
+            counter++
+        }
+    }
+
+    @Test
+    fun nestedScroll_twoNodes_hierarchyDispatch() {
+        val preScrollReturn = Offset(60f, 30f)
+        val preFlingReturn = Velocity(Offset(154f, 56f))
+        var currentsource = NestedScrollSource.Drag
+
+        val childConnection = object : NestedScrollConnection {}
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(available).isEqualTo(preScrollOffset)
+                assertThat(source).isEqualTo(currentsource)
+                return preScrollReturn
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(consumed).isEqualTo(scrollOffset)
+                assertThat(available).isEqualTo(scrollLeftOffset)
+                assertThat(source).isEqualTo(currentsource)
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(available).isEqualTo(preFling)
+                return preFlingReturn
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(consumed).isEqualTo(postFlingConsumed)
+                assertThat(available).isEqualTo(postFlingLeft)
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                Box(
+                    Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+            assertThat(preRes).isEqualTo(preScrollReturn)
+
+            childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+            // flip to fling to test again below
+            currentsource = NestedScrollSource.Fling
+        }
+
+        rule.runOnIdle {
+            val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+            assertThat(preRes).isEqualTo(preScrollReturn)
+
+            childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+        }
+
+        rule.runOnIdle {
+            val preFlingRes = childDispatcher.dispatchPreFling(preFling)
+            assertThat(preFlingRes).isEqualTo(preFlingReturn)
+            childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+        }
+    }
+
+    @Test
+    fun nestedScroll_deltaCalculation_preScroll() {
+        val dispatchedPreScroll = Offset(10f, 10f)
+        val grandParentConsumesPreScroll = Offset(2f, 2f)
+        val parentConsumedPreScroll = Offset(1f, 1f)
+
+        val childConnection = object : NestedScrollConnection {}
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(available).isEqualTo(dispatchedPreScroll)
+                return grandParentConsumesPreScroll
+            }
+        }
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(available)
+                    .isEqualTo(dispatchedPreScroll - grandParentConsumesPreScroll)
+                return parentConsumedPreScroll
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            val preRes =
+                childDispatcher.dispatchPreScroll(dispatchedPreScroll, NestedScrollSource.Drag)
+            assertThat(preRes).isEqualTo(grandParentConsumesPreScroll + parentConsumedPreScroll)
+        }
+    }
+
+    @Test
+    fun nestedScroll_deltaCalculation_scroll() {
+        val dispatchedConsumedScroll = Offset(4f, 4f)
+        val dispatchedScroll = Offset(10f, 10f)
+        val grandParentConsumedScroll = Offset(2f, 2f)
+        val parentConsumedScroll = Offset(1f, 1f)
+
+        val childConnection = object : NestedScrollConnection {}
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(consumed).isEqualTo(parentConsumedScroll + dispatchedConsumedScroll)
+                assertThat(available).isEqualTo(dispatchedScroll - parentConsumedScroll)
+                return grandParentConsumedScroll
+            }
+        }
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(consumed).isEqualTo(dispatchedConsumedScroll)
+                assertThat(available).isEqualTo(dispatchedScroll)
+                return parentConsumedScroll
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            childDispatcher.dispatchPostScroll(
+                dispatchedConsumedScroll,
+                dispatchedScroll,
+                NestedScrollSource.Drag
+            )
+        }
+    }
+
+    @Test
+    fun nestedScroll_deltaCalculation_preFling() {
+        val dispatchedVelocity = Velocity(Offset(10f, 10f))
+        val grandParentConsumesPreFling = Velocity(Offset(2f, 2f))
+        val parentConsumedPreFling = Velocity(Offset(1f, 1f))
+
+        val childConnection = object : NestedScrollConnection {}
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(available).isEqualTo(dispatchedVelocity)
+                return grandParentConsumesPreFling
+            }
+        }
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(available)
+                    .isEqualTo(dispatchedVelocity - grandParentConsumesPreFling)
+                return parentConsumedPreFling
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            val preRes = childDispatcher.dispatchPreFling(dispatchedVelocity)
+            assertThat(preRes).isEqualTo(grandParentConsumesPreFling + parentConsumedPreFling)
+        }
+    }
+
+    @Test
+    fun nestedScroll_deltaCalculation_fling() {
+        val dispatchedConsumedVelocity = Velocity(Offset(4f, 4f))
+        val dispatchedLeftVelocity = Velocity(Offset(10f, 10f))
+        val grandParentConsumedPostFling = Velocity(Offset(2f, 2f))
+        val parentConsumedPostFling = Velocity(Offset(1f, 1f))
+
+        val childConnection = object : NestedScrollConnection {}
+        val grandParentConnection = object : NestedScrollConnection {
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(consumed)
+                    .isEqualTo(parentConsumedPostFling + dispatchedConsumedVelocity)
+                assertThat(available)
+                    .isEqualTo(dispatchedLeftVelocity - parentConsumedPostFling)
+                return onFinished(grandParentConsumedPostFling)
+            }
+        }
+        val parentConnection = object : NestedScrollConnection {
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(consumed).isEqualTo(dispatchedConsumedVelocity)
+                assertThat(available).isEqualTo(dispatchedLeftVelocity)
+                onFinished(parentConsumedPostFling)
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+                    Box(
+                        Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            childDispatcher.dispatchPostFling(dispatchedConsumedVelocity, dispatchedLeftVelocity)
+        }
+    }
+
+    @Test
+    fun nestedScroll_twoNodes_flatDispatch() {
+        val preScrollReturn = Offset(60f, 30f)
+        val preFlingReturn = Velocity(Offset(154f, 56f))
+        var currentsource = NestedScrollSource.Drag
+
+        val childConnection = object : NestedScrollConnection {}
+        val parentConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(available).isEqualTo(preScrollOffset)
+                assertThat(source).isEqualTo(currentsource)
+                return preScrollReturn
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(consumed).isEqualTo(scrollOffset)
+                assertThat(available).isEqualTo(scrollLeftOffset)
+                assertThat(source).isEqualTo(currentsource)
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(available).isEqualTo(preFling)
+                return preFlingReturn
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(consumed).isEqualTo(postFlingConsumed)
+                assertThat(available).isEqualTo(postFlingLeft)
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(
+                Modifier
+                    .size(100.dp)
+                    .nestedScroll(parentConnection) // parent
+                    .nestedScroll(childConnection, childDispatcher) // child
+            )
+        }
+
+        rule.runOnIdle {
+            val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+            assertThat(preRes).isEqualTo(preScrollReturn)
+
+            childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+            // flip to fling to test again below
+            currentsource = NestedScrollSource.Fling
+        }
+
+        rule.runOnIdle {
+            val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+            assertThat(preRes).isEqualTo(preScrollReturn)
+
+            childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+        }
+
+        rule.runOnIdle {
+            val preFlingRes = childDispatcher.dispatchPreFling(preFling)
+            assertThat(preFlingRes).isEqualTo(preFlingReturn)
+            childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+        }
+    }
+
+    @Test
+    fun nestedScroll_shouldNotCalledSelfConnection() {
+        val childConnection = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertWithMessage("self connection shouldn't be called").fail()
+                return Offset.Zero
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertWithMessage("self connection shouldn't be called").fail()
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertWithMessage("self connection shouldn't be called").fail()
+                return Velocity.Zero
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertWithMessage("self connection shouldn't be called").fail()
+            }
+        }
+        val parentConnection = object : NestedScrollConnection {}
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            Box(Modifier.nestedScroll(parentConnection)) {
+                Box(Modifier.nestedScroll(childConnection, childDispatcher))
+            }
+        }
+
+        rule.runOnIdle {
+            childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            childDispatcher
+                .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Fling)
+        }
+
+        rule.runOnIdle {
+            childDispatcher.dispatchPreFling(preFling)
+            childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+        }
+    }
+
+    @Test
+    fun nestedScroll_hierarchyDispatch_rootParentRemoval() {
+        testRootParentAdditionRemoval { root, child ->
+            Box(Modifier.size(100.dp).then(root)) {
+                Box(child)
+            }
+        }
+    }
+
+    @Test
+    fun nestedScroll_flatDispatch_rootParentRemoval() {
+        testRootParentAdditionRemoval { root, child ->
+            Box(Modifier.then(root).then(child))
+        }
+    }
+
+    @Test
+    fun nestedScroll_flatDispatch_longChain_rootParentRemoval() {
+        testRootParentAdditionRemoval { root, child ->
+            // insert a few random modifiers so it's more realistic example of wrapper re-usage
+            Box(Modifier.size(100.dp).then(root).padding(5.dp).size(50.dp).then(child))
+        }
+    }
+
+    @Test
+    fun nestedScroll_hierarchyDispatch_middleParentRemoval() {
+        testMiddleParentAdditionRemoval { rootMod, middleMod, childMod ->
+            // random boxes to emulate nesting
+            Box(Modifier.size(100.dp).then(rootMod)) {
+                Box {
+                    Box(Modifier.size(100.dp).then(middleMod)) {
+                        Box {
+                            Box(childMod)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun nestedScroll_flatDispatch_middleParentRemoval() {
+        testMiddleParentAdditionRemoval { rootMod, middleMod, childMod ->
+            Box(
+                Modifier
+                    .then(rootMod)
+                    .then(middleMod)
+                    .then(childMod)
+            )
+        }
+    }
+
+    @Test
+    fun nestedScroll_flatDispatch_longChain_middleParentRemoval() {
+        testMiddleParentAdditionRemoval { rootMod, middleMod, childMod ->
+            // insert a few random modifiers so it's more realistic example of wrapper re-usage
+            Box(
+                Modifier
+                    .size(100.dp)
+                    .then(rootMod)
+                    .size(90.dp)
+                    .clipToBounds()
+                    .then(middleMod)
+                    .padding(5.dp)
+                    .then(childMod)
+            )
+        }
+    }
+
+    @Test
+    fun nestedScroll_flatDispatch_runtimeSwapChange_orderTest() {
+        val preScrollReturn = Offset(60f, 30f)
+        val preFlingReturn = Velocity(Offset(154f, 56f))
+        var counter = 0
+
+        val isConnection1Parent = mutableStateOf(true)
+        val childConnection = object : NestedScrollConnection {}
+        val connection1 = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                return preScrollReturn
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                return preFlingReturn
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                onFinished.invoke(Velocity.Zero)
+            }
+        }
+        val connection2 = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                return preScrollReturn
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                return preFlingReturn
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                onFinished.invoke(Velocity.Zero)
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            val nestedScrollParents = if (isConnection1Parent.value) {
+                Modifier.nestedScroll(connection1).nestedScroll(connection2)
+            } else {
+                Modifier.nestedScroll(connection2).nestedScroll(connection1)
+            }
+            Box(
+                Modifier
+                    .size(100.dp)
+                    .then(nestedScrollParents)
+                    .nestedScroll(childConnection, childDispatcher)
+            )
+        }
+
+        repeat(2) {
+            rule.runOnIdle {
+                counter = 1
+
+                childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                childDispatcher.dispatchPostScroll(
+                    scrollOffset,
+                    scrollLeftOffset,
+                    NestedScrollSource.Drag
+                )
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                childDispatcher.dispatchPreFling(preFling)
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                isConnection1Parent.value = !isConnection1Parent.value
+            }
+        }
+    }
+
+    @Test
+    fun nestedScroll_hierarchyDispatch_runtimeSwapChange_orderTest() {
+        val preScrollReturn = Offset(60f, 30f)
+        val preFlingReturn = Velocity(Offset(154f, 56f))
+        var counter = 0
+
+        val isConnection1Parent = mutableStateOf(true)
+        val childConnection = object : NestedScrollConnection {}
+        val connection1 = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                return preScrollReturn
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                return preFlingReturn
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                onFinished.invoke(Velocity.Zero)
+            }
+        }
+        val connection2 = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                return preScrollReturn
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                return Offset.Zero
+            }
+
+            override fun onPreFling(available: Velocity): Velocity {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+                counter++
+                return preFlingReturn
+            }
+
+            override fun onPostFling(
+                consumed: Velocity,
+                available: Velocity,
+                onFinished: (Velocity) -> Unit
+            ) {
+                assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+                counter++
+                onFinished.invoke(Velocity.Zero)
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            val outerBoxConnection = if (isConnection1Parent.value) connection1 else connection2
+            val innerBoxConnection = if (isConnection1Parent.value) connection2 else connection1
+            Box(Modifier.size(100.dp).nestedScroll(outerBoxConnection)) {
+                Box(Modifier.size(100.dp).nestedScroll(innerBoxConnection)) {
+                    Box(Modifier.nestedScroll(childConnection, childDispatcher))
+                }
+            }
+        }
+
+        repeat(2) {
+            rule.runOnIdle {
+                counter = 1
+
+                childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                childDispatcher.dispatchPostScroll(
+                    scrollOffset,
+                    scrollLeftOffset,
+                    NestedScrollSource.Drag
+                )
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                childDispatcher.dispatchPreFling(preFling)
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+                assertThat(counter).isEqualTo(3)
+                counter = 1
+
+                isConnection1Parent.value = !isConnection1Parent.value
+            }
+        }
+    }
+
+    // helper functions
+
+    private fun testMiddleParentAdditionRemoval(
+        content: @Composable (root: Modifier, middle: Modifier, child: Modifier) -> Unit
+    ) {
+        val rootParentPreConsumed = Offset(60f, 30f)
+        val parentToRemovePreConsumed = Offset(21f, 44f)
+
+        val emitNewParent = mutableStateOf(true)
+        val childConnection = object : NestedScrollConnection {}
+        val rootParent = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                return rootParentPreConsumed
+            }
+        }
+        val parentToRemove = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                if (!emitNewParent.value) {
+                    assertWithMessage("Shouldn't be called when not emitted").fail()
+                }
+                return parentToRemovePreConsumed
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            val maybeNestedScroll =
+                if (emitNewParent.value) Modifier.nestedScroll(parentToRemove) else Modifier
+            content.invoke(
+                Modifier.nestedScroll(rootParent),
+                maybeNestedScroll,
+                Modifier.nestedScroll(childConnection, childDispatcher)
+            )
+        }
+
+        rule.runOnIdle {
+            val res =
+                childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(res).isEqualTo(rootParentPreConsumed + parentToRemovePreConsumed)
+
+            emitNewParent.value = false
+        }
+
+        rule.runOnIdle {
+            val res =
+                childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(res).isEqualTo(rootParentPreConsumed)
+
+            emitNewParent.value = true
+        }
+
+        rule.runOnIdle {
+            val res =
+                childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(res).isEqualTo(rootParentPreConsumed + parentToRemovePreConsumed)
+        }
+    }
+
+    private fun testRootParentAdditionRemoval(
+        content: @Composable (root: Modifier, child: Modifier) -> Unit
+    ) {
+        val preScrollReturn = Offset(60f, 30f)
+
+        val emitParentNestedScroll = mutableStateOf(true)
+        val childConnection = object : NestedScrollConnection {}
+        val parent = object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                return preScrollReturn
+            }
+        }
+        val childDispatcher = NestedScrollDispatcher()
+        rule.setContent {
+            val maybeNestedScroll =
+                if (emitParentNestedScroll.value) Modifier.nestedScroll(parent) else Modifier
+            content.invoke(
+                maybeNestedScroll,
+                Modifier.nestedScroll(childConnection, childDispatcher)
+            )
+        }
+
+        rule.runOnIdle {
+            val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(res).isEqualTo(preScrollReturn)
+
+            emitParentNestedScroll.value = false
+        }
+
+        rule.runOnIdle {
+            val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(res).isEqualTo(Offset.Zero)
+            emitParentNestedScroll.value = true
+        }
+
+        rule.runOnIdle {
+            val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+            assertThat(res).isEqualTo(preScrollReturn)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
index 8ed2323..76686bf 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.setFocusableContent
 import androidx.compose.ui.focusRequester
@@ -46,10 +45,6 @@
  */
 @SmallTest
 @RunWith(Parameterized::class)
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class
-)
 class AndroidProcessKeyInputTest(val keyEventAction: Int) {
     @get:Rule
     val rule = createComposeRule()
@@ -72,7 +67,7 @@
                 modifier = Modifier
                     .focusRequester(focusRequester)
                     .focus()
-                    .keyInputFilter {
+                    .onKeyEvent {
                         receivedKeyEvent = it
                         true
                     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
index da95e3d..fdc0120 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
@@ -25,7 +25,6 @@
  * The [KeyEvent] is usually created by the system. This function creates an instance of
  * [KeyEvent] that can be used in tests.
  */
-@OptIn(ExperimentalKeyInput::class)
 fun keyEvent(key: Key, keyEventType: KeyEventType, androidMetaKeys: Int = 0): KeyEvent {
     val action = when (keyEventType) {
         KeyEventType.KeyDown -> ACTION_DOWN
@@ -40,7 +39,6 @@
  *  [KeyEventAndroid] inline classes do not allow
  *  overriding the equals() function.  So we use this util function to compare KeyEvents.
  */
-@OptIn(ExperimentalKeyInput::class)
 fun KeyEvent.assertEqualTo(expected: KeyEvent) {
     Truth.assertThat(key).isEqualTo(expected.key)
     Truth.assertThat(type).isEqualTo(expected.type)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
index d5ca9df..ded752b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
@@ -28,7 +28,6 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalKeyInput::class)
 class MetaKeyTest {
 
     @Test
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
index a95401c..8f44a6b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
@@ -19,7 +19,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.setFocusableContent
 import androidx.compose.ui.focusRequester
@@ -38,10 +37,6 @@
 @Suppress("DEPRECATION")
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class
-)
 class ProcessKeyInputTest {
     @get:Rule
     val rule = createComposeRule()
@@ -61,7 +56,7 @@
     fun noFocusModifier_throwsException() {
         // Arrange.
         rule.setFocusableContent {
-            Box(modifier = Modifier.keyInputFilter { true })
+            Box(modifier = Modifier.onKeyEvent { true })
         }
 
         // Act.
@@ -73,7 +68,7 @@
 
         // Arrange.
         rule.setFocusableContent {
-            Box(modifier = Modifier.focus().keyInputFilter { true })
+            Box(modifier = Modifier.focus().onKeyEvent { true })
         }
 
         // Act.
@@ -90,7 +85,7 @@
                 modifier = Modifier
                     .focusRequester(focusRequester)
                     .focus()
-                    .keyInputFilter {
+                    .onKeyEvent {
                         receivedKeyEvent = it
                         true
                     }
@@ -120,7 +115,7 @@
                 modifier = Modifier
                     .focusRequester(focusRequester)
                     .focus()
-                    .previewKeyInputFilter {
+                    .onPreviewKeyEvent {
                         receivedKeyEvent = it
                         true
                     }
@@ -151,11 +146,11 @@
                 modifier = Modifier
                     .focusRequester(focusRequester)
                     .focus()
-                    .keyInputFilter {
+                    .onKeyEvent {
                         receivedKeyEvent = it
                         true
                     }
-                    .previewKeyInputFilter {
+                    .onPreviewKeyEvent {
                         receivedPreviewKeyEvent = it
                         true
                     }
@@ -187,11 +182,11 @@
                 modifier = Modifier
                     .focusRequester(focusRequester)
                     .focus()
-                    .keyInputFilter {
+                    .onKeyEvent {
                         onKeyEventTrigger = triggerIndex++
                         true
                     }
-                    .previewKeyInputFilter {
+                    .onPreviewKeyEvent {
                         onPreviewKeyEventTrigger = triggerIndex++
                         false
                     }
@@ -224,11 +219,11 @@
             Box(
                 modifier = Modifier
                     .focus()
-                    .keyInputFilter {
+                    .onKeyEvent {
                         parentOnKeyEventTrigger = triggerIndex++
                         false
                     }
-                    .previewKeyInputFilter {
+                    .onPreviewKeyEvent {
                         parentOnPreviewKeyEventTrigger = triggerIndex++
                         false
                     }
@@ -237,11 +232,11 @@
                     modifier = Modifier
                         .focusRequester(focusRequester)
                         .focus()
-                        .keyInputFilter {
+                        .onKeyEvent {
                             childOnKeyEventTrigger = triggerIndex++
                             false
                         }
-                        .previewKeyInputFilter {
+                        .onPreviewKeyEvent {
                             childOnPreviewKeyEventTrigger = triggerIndex++
                             false
                         }
@@ -276,11 +271,11 @@
         rule.setFocusableContent {
             Box(
                 modifier = Modifier
-                    .keyInputFilter {
+                    .onKeyEvent {
                         parentOnKeyEventTrigger = triggerIndex++
                         false
                     }
-                    .previewKeyInputFilter {
+                    .onPreviewKeyEvent {
                         parentOnPreviewKeyEventTrigger = triggerIndex++
                         false
                     }
@@ -289,11 +284,11 @@
                     modifier = Modifier
                         .focusRequester(focusRequester)
                         .focus()
-                        .keyInputFilter {
+                        .onKeyEvent {
                             childOnKeyEventTrigger = triggerIndex++
                             false
                         }
-                        .previewKeyInputFilter {
+                        .onPreviewKeyEvent {
                             childOnPreviewKeyEventTrigger = triggerIndex++
                             false
                         }
@@ -331,11 +326,11 @@
             Box(
                 modifier = Modifier
                     .focus()
-                    .keyInputFilter {
+                    .onKeyEvent {
                         grandParentOnKeyEventTrigger = triggerIndex++
                         false
                     }
-                    .previewKeyInputFilter {
+                    .onPreviewKeyEvent {
                         grandParentOnPreviewKeyEventTrigger = triggerIndex++
                         false
                     }
@@ -343,11 +338,11 @@
                 Box(
                     modifier = Modifier
                         .focus()
-                        .keyInputFilter {
+                        .onKeyEvent {
                             parentOnKeyEventTrigger = triggerIndex++
                             false
                         }
-                        .previewKeyInputFilter {
+                        .onPreviewKeyEvent {
                             parentOnPreviewKeyEventTrigger = triggerIndex++
                             false
                         }
@@ -356,11 +351,11 @@
                         modifier = Modifier
                             .focusRequester(focusRequester)
                             .focus()
-                            .keyInputFilter {
+                            .onKeyEvent {
                                 childOnKeyEventTrigger = triggerIndex++
                                 false
                             }
-                            .previewKeyInputFilter {
+                            .onPreviewKeyEvent {
                                 childOnPreviewKeyEventTrigger = triggerIndex++
                                 false
                             }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 70908d0..e209584 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -16,20 +16,18 @@
 
 package androidx.compose.ui.input.pointer
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.autofill.Autofill
 import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNodeWrapper
@@ -84,7 +82,6 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalLayoutNodeApi::class)
 class PointerInputEventProcessorTest {
 
     private lateinit var root: LayoutNode
@@ -3094,7 +3091,6 @@
 abstract class TestOwner : Owner {
     var position: IntOffset? = null
 
-    @ExperimentalLayoutNodeApi
     override val root: LayoutNode
         get() = LayoutNode()
 
@@ -3106,7 +3102,6 @@
 private class PointerInputModifierImpl2(override val pointerInputFilter: PointerInputFilter) :
     PointerInputModifier
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 private fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
     LayoutNode().apply {
         this.modifier = modifier
@@ -3130,27 +3125,19 @@
         detach()
     }
 
-@ExperimentalLayoutNodeApi
 private fun mockOwner(
     position: IntOffset = IntOffset.Zero,
     targetRoot: LayoutNode = LayoutNode()
 ): Owner = MockOwner(position, targetRoot)
 
-@ExperimentalLayoutNodeApi
-@OptIn(
-    ExperimentalFocus::class,
-    InternalCoreApi::class
-)
+@OptIn(ExperimentalComposeUiApi::class, InternalCoreApi::class)
 private class MockOwner(
     private val position: IntOffset,
     private val targetRoot: LayoutNode
 ) : Owner {
     override fun calculatePosition(): IntOffset = position
     override fun requestFocus(): Boolean = false
-
-    @ExperimentalKeyInput
     override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
-
     override val root: LayoutNode
         get() = targetRoot
     override val hapticFeedBack: HapticFeedback
@@ -3187,8 +3174,6 @@
     override fun onRequestRelayout(layoutNode: LayoutNode) {
     }
 
-    override val hasPendingMeasureOrLayout = false
-
     override fun onAttach(node: LayoutNode) {
     }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
index cc1b366..917daa9 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.ValueElement
 import androidx.compose.ui.platform.ViewConfiguration
@@ -31,22 +30,24 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runBlockingTest
 import kotlinx.coroutines.withTimeout
-import kotlinx.coroutines.yield
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalPointerInput::class)
+@OptIn(ExperimentalCoroutinesApi::class)
 class SuspendingPointerInputFilterTest {
     @After
     fun after() {
@@ -55,18 +56,17 @@
     }
 
     @Test
-    fun testAwaitSingleEvent(): Unit = runBlocking {
-        val filter = SuspendingPointerInputFilter(DummyViewConfiguration())
+    fun testAwaitSingleEvent(): Unit = runBlockingTest {
+        val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
 
         val result = CompletableDeferred<PointerEvent>()
         launch {
             with(filter) {
-                handlePointerInput {
+                awaitPointerEventScope {
                     result.complete(awaitPointerEvent())
                 }
             }
         }
-        yield()
 
         val emitter = PointerInputChangeEmitter()
         val expectedChange = emitter.nextChange(Offset(5f, 5f))
@@ -85,12 +85,12 @@
     }
 
     @Test
-    fun testAwaitSeveralEvents(): Unit = runBlocking {
-        val filter = SuspendingPointerInputFilter(DummyViewConfiguration())
+    fun testAwaitSeveralEvents(): Unit = runBlockingTest {
+        val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
         val results = Channel<PointerEvent>(Channel.UNLIMITED)
-        val reader = launch {
+        launch {
             with(filter) {
-                handlePointerInput {
+                awaitPointerEventScope {
                     repeat(3) {
                         results.offer(awaitPointerEvent())
                     }
@@ -98,7 +98,6 @@
                 }
             }
         }
-        yield()
 
         val emitter = PointerInputChangeEmitter()
         val expected = listOf(
@@ -118,17 +117,15 @@
         }
 
         assertEquals(expected, received)
-
-        reader.cancel()
     }
 
     @Test
-    fun testSyntheticCancelEvent(): Unit = runBlocking {
-        val filter = SuspendingPointerInputFilter(DummyViewConfiguration())
+    fun testSyntheticCancelEvent(): Unit = runBlockingTest {
+        val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
         val results = Channel<PointerEvent>(Channel.UNLIMITED)
-        val reader = launch {
+        launch {
             with(filter) {
-                handlePointerInput {
+                awaitPointerEventScope {
                     repeat(3) {
                         results.offer(awaitPointerEvent())
                     }
@@ -136,7 +133,6 @@
                 }
             }
         }
-        yield()
 
         val bounds = IntSize(50, 50)
         val emitter1 = PointerInputChangeEmitter(0)
@@ -193,8 +189,33 @@
             val actualEvent = received[index]
             PointerEventSubject.assertThat(actualEvent).isStructurallyEqualTo(expectedEvent)
         }
+    }
 
-        reader.cancel()
+    @Test
+    fun testCancelledHandlerBlock() = runBlockingTest {
+        val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
+        val counter = TestCounter()
+        val handler = launch {
+            with(filter) {
+                try {
+                    awaitPointerEventScope {
+                        try {
+                            counter.expect(1, "about to call awaitPointerEvent")
+                            awaitPointerEvent()
+                            fail("awaitPointerEvent returned; should have thrown for cancel")
+                        } finally {
+                            counter.expect(3, "inner finally block running")
+                        }
+                    }
+                } finally {
+                    counter.expect(4, "outer finally block running; inner finally should have run")
+                }
+            }
+        }
+
+        counter.expect(2, "before cancelling handler; awaitPointerEvent should be suspended")
+        handler.cancel()
+        counter.expect(5, "after cancelling; finally blocks should have run")
     }
 
     @Test
@@ -213,7 +234,6 @@
 
 private fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
 
-@ExperimentalPointerInput
 private val PointerEvent.firstChange get() = changes.first()
 
 private class PointerInputChangeEmitter(id: Int = 0) {
@@ -244,7 +264,7 @@
     }
 }
 
-private class DummyViewConfiguration : ViewConfiguration {
+private class FakeViewConfiguration : ViewConfiguration {
     override val longPressTimeout: Duration
         get() = 500.milliseconds
     override val doubleTapTimeout: Duration
@@ -253,4 +273,16 @@
         get() = 40.milliseconds
     override val touchSlop: Float
         get() = 18f
-}
\ No newline at end of file
+}
+
+private class TestCounter {
+    private var count = 0
+
+    fun expect(checkpoint: Int, message: String = "(no message)") {
+        val expected = count + 1
+        if (checkpoint != expected) {
+            fail("out of order event $checkpoint, expected $expected, $message")
+        }
+        count = expected
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
index 9b4325a..04da750 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.layout
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import org.junit.Test
@@ -26,7 +25,6 @@
 @RunWith(AndroidJUnit4::class)
 class AlignmentLineTest {
     @Test
-    @OptIn(ExperimentalLayoutNodeApi::class)
     fun queryingLinesOfUnmeasuredChild() {
         val root = root {
             queryAlignmentLineDuringMeasure()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index fb78806..65a92f9 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.layout
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.MeasureAndLayoutDelegate
 import androidx.compose.ui.node.OwnerSnapshotObserver
@@ -30,7 +29,6 @@
 import kotlin.math.min
 
 @Suppress("UNCHECKED_CAST")
-@ExperimentalLayoutNodeApi
 internal fun createDelegate(
     root: LayoutNode,
     firstMeasureCompleted: Boolean = true
@@ -65,7 +63,6 @@
 
 internal fun defaultRootConstraints() = Constraints(maxWidth = 100, maxHeight = 100)
 
-@ExperimentalLayoutNodeApi
 internal fun assertNotRemeasured(node: LayoutNode, block: (LayoutNode) -> Unit) {
     val measuresCountBefore = node.measuresCount
     block(node)
@@ -73,7 +70,6 @@
     assertMeasuredAndLaidOut(node)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun assertRemeasured(
     node: LayoutNode,
     times: Int = 1,
@@ -89,7 +85,6 @@
     assertMeasuredAndLaidOut(node)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun assertRelaidOut(node: LayoutNode, times: Int = 1, block: (LayoutNode) -> Unit) {
     val layoutsCountBefore = node.layoutsCount
     block(node)
@@ -97,7 +92,6 @@
     assertMeasuredAndLaidOut(node)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun assertNotRelaidOut(node: LayoutNode, block: (LayoutNode) -> Unit) {
     val layoutsCountBefore = node.layoutsCount
     block(node)
@@ -105,17 +99,14 @@
     assertMeasuredAndLaidOut(node)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun assertMeasureRequired(node: LayoutNode) {
     Truth.assertThat(node.layoutState).isEqualTo(LayoutNode.LayoutState.NeedsRemeasure)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun assertMeasuredAndLaidOut(node: LayoutNode) {
     Truth.assertThat(node.layoutState).isEqualTo(LayoutNode.LayoutState.Ready)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun assertLayoutRequired(node: LayoutNode) {
     Truth.assertThat(node.layoutState).isEqualTo(LayoutNode.LayoutState.NeedsRelayout)
 }
@@ -147,12 +138,10 @@
     Truth.assertThat(modifier.layoutsCount).isEqualTo(layoutsCountBefore + 1)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun root(block: LayoutNode.() -> Unit = {}): LayoutNode {
     return node(block)
 }
 
-@ExperimentalLayoutNodeApi
 internal fun node(block: LayoutNode.() -> Unit = {}): LayoutNode {
     return LayoutNode().apply {
         measureBlocks = MeasureInMeasureBlock()
@@ -160,66 +149,51 @@
     }
 }
 
-@ExperimentalLayoutNodeApi
 internal fun LayoutNode.add(child: LayoutNode) = insertAt(children.count(), child)
 
-@ExperimentalLayoutNodeApi
 internal fun LayoutNode.measureInLayoutBlock() {
     measureBlocks = MeasureInLayoutBlock()
 }
 
-@ExperimentalLayoutNodeApi
 internal fun LayoutNode.doNotMeasure() {
     measureBlocks = NoMeasureBlock()
 }
 
-@ExperimentalLayoutNodeApi
 internal fun LayoutNode.queryAlignmentLineDuringMeasure() {
     (measureBlocks as SmartMeasureBlock).queryAlignmentLinesDuringMeasure = true
 }
 
-@ExperimentalLayoutNodeApi
 internal fun LayoutNode.runDuringMeasure(block: () -> Unit) {
     (measureBlocks as SmartMeasureBlock).preMeasureCallback = block
 }
 
-@ExperimentalLayoutNodeApi
 internal fun LayoutNode.runDuringLayout(block: () -> Unit) {
     (measureBlocks as SmartMeasureBlock).preLayoutCallback = block
 }
 
-@ExperimentalLayoutNodeApi
 internal val LayoutNode.first: LayoutNode get() = children.first()
-@ExperimentalLayoutNodeApi
 internal val LayoutNode.second: LayoutNode get() = children[1]
-@ExperimentalLayoutNodeApi
 internal val LayoutNode.measuresCount: Int
     get() = (measureBlocks as SmartMeasureBlock).measuresCount
-@ExperimentalLayoutNodeApi
 internal val LayoutNode.layoutsCount: Int
     get() = (measureBlocks as SmartMeasureBlock).layoutsCount
-@ExperimentalLayoutNodeApi
 internal var LayoutNode.wrapChildren: Boolean
     get() = (measureBlocks as SmartMeasureBlock).wrapChildren
     set(value) {
         (measureBlocks as SmartMeasureBlock).wrapChildren = value
     }
-@ExperimentalLayoutNodeApi
 internal val LayoutNode.measuredWithLayoutDirection: LayoutDirection
     get() = (measureBlocks as SmartMeasureBlock).measuredLayoutDirection!!
-@ExperimentalLayoutNodeApi
 internal var LayoutNode.size: Int?
     get() = (measureBlocks as SmartMeasureBlock).size
     set(value) {
         (measureBlocks as SmartMeasureBlock).size = value
     }
-@ExperimentalLayoutNodeApi
 internal var LayoutNode.childrenDirection: LayoutDirection?
     get() = (measureBlocks as SmartMeasureBlock).childrenLayoutDirection
     set(value) {
         (measureBlocks as SmartMeasureBlock).childrenLayoutDirection = value
     }
-@ExperimentalLayoutNodeApi
 internal var LayoutNode.shouldPlaceChildren: Boolean
     get() = (measureBlocks as SmartMeasureBlock).shouldPlaceChildren
     set(value) {
@@ -228,7 +202,6 @@
 
 internal val TestAlignmentLine = HorizontalAlignmentLine(::min)
 
-@ExperimentalLayoutNodeApi
 internal abstract class SmartMeasureBlock : LayoutNode.NoIntrinsicsMeasureBlocks("") {
     var measuresCount = 0
         protected set
@@ -246,7 +219,6 @@
     var shouldPlaceChildren = true
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class MeasureInMeasureBlock : SmartMeasureBlock() {
     override fun measure(
         measureScope: MeasureScope,
@@ -292,7 +264,6 @@
     }
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class MeasureInLayoutBlock : SmartMeasureBlock() {
 
     override var wrapChildren: Boolean
@@ -342,7 +313,6 @@
     }
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class NoMeasureBlock : SmartMeasureBlock() {
 
     override var queryAlignmentLinesDuringMeasure: Boolean
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
index 1319672..9136657 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.layout
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode.LayoutState
 import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
 import androidx.compose.ui.unit.Constraints
@@ -29,7 +28,6 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalLayoutNodeApi::class)
 class MeasureAndLayoutDelegateTest {
 
     private val DifferentSize = 50
@@ -1098,7 +1096,6 @@
         val root = root {
             add(
                 node {
-                    @OptIn(ExperimentalLayoutNodeApi::class)
                     modifier = spyModifier
                 }
             )
@@ -1118,7 +1115,6 @@
         val root = root {
             add(
                 node {
-                    @OptIn(ExperimentalLayoutNodeApi::class)
                     modifier = spyModifier
                 }
             )
@@ -1168,7 +1164,6 @@
                     delegate.requestRelayout(root)
                     root.runDuringLayout {
                         // this means the root.first will be measured before laying out the root
-                        @OptIn(ExperimentalLayoutNodeApi::class)
                         assertThat(root.first.layoutState).isEqualTo(LayoutState.NeedsRelayout)
                     }
                     assertThat(delegate.measureAndLayout()).isFalse()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
index baaf1c4..2f05d4f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
@@ -18,11 +18,16 @@
 
 import android.view.View
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import android.widget.LinearLayout
 import android.widget.ScrollView
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Recomposer
+import androidx.compose.runtime.State
 import androidx.compose.runtime.emptyContent
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -43,6 +48,7 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
@@ -53,6 +59,7 @@
 import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import kotlin.math.min
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -430,6 +437,226 @@
 
         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
     }
+
+    @Test
+    fun simplePadding() {
+        val paddingLeftPx = 100.0f
+        val paddingTopPx = 120.0f
+        var realLeft: Float? = null
+        var realTop: Float? = null
+
+        val positionedLatch = CountDownLatch(1)
+        rule.runOnUiThread {
+            activity.setContent {
+                with(AmbientDensity.current) {
+                    Box(
+                        Modifier.fillMaxSize()
+                            .padding(start = paddingLeftPx.toDp(), top = paddingTopPx.toDp())
+                            .onGloballyPositioned {
+                                realLeft = it.positionInParent.x
+                                realTop = it.positionInParent.y
+                                positionedLatch.countDown()
+                            }
+                    )
+                }
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        assertThat(paddingLeftPx).isEqualTo(realLeft)
+        assertThat(paddingTopPx).isEqualTo(realTop)
+    }
+    @Test
+    fun nestedLayoutCoordinates() {
+        val firstPaddingPx = 10f
+        val secondPaddingPx = 20f
+        val thirdPaddingPx = 30f
+        var gpCoordinates: LayoutCoordinates? = null
+        var childCoordinates: LayoutCoordinates? = null
+
+        val positionedLatch = CountDownLatch(2)
+        rule.runOnUiThread {
+            activity.setContent {
+                with(AmbientDensity.current) {
+                    Box(
+                        Modifier.padding(start = firstPaddingPx.toDp()).then(
+                            Modifier.onGloballyPositioned {
+                                gpCoordinates = it
+                                positionedLatch.countDown()
+                            }
+                        )
+                    ) {
+                        Box(Modifier.padding(start = secondPaddingPx.toDp())) {
+                            Box(
+                                Modifier.fillMaxSize()
+                                    .padding(start = thirdPaddingPx.toDp())
+                                    .onGloballyPositioned {
+                                        childCoordinates = it
+                                        positionedLatch.countDown()
+                                    }
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        // global position
+        val gPos = childCoordinates!!.localToGlobal(Offset.Zero).x
+        assertThat(gPos).isEqualTo((firstPaddingPx + secondPaddingPx + thirdPaddingPx))
+        // Position in grandparent Px(value=50.0)
+        val gpPos = gpCoordinates!!.childToLocal(childCoordinates!!, Offset.Zero).x
+        assertThat(gpPos).isEqualTo((secondPaddingPx + thirdPaddingPx))
+        // local position
+        assertThat(childCoordinates!!.positionInParent.x).isEqualTo(thirdPaddingPx)
+    }
+
+    @Test
+    fun globalCoordinatesAreInActivityCoordinates() {
+        val padding = 30
+        val localPosition = androidx.compose.ui.geometry.Offset.Zero
+        val framePadding = Offset(padding.toFloat(), padding.toFloat())
+        var realGlobalPosition: Offset? = null
+        var realLocalPosition: Offset? = null
+        var frameGlobalPosition: Offset? = null
+
+        val positionedLatch = CountDownLatch(1)
+        rule.runOnUiThread {
+            val frameLayout = FrameLayout(activity)
+            frameLayout.setPadding(padding, padding, padding, padding)
+            activity.setContentView(frameLayout)
+
+            val position = IntArray(2)
+            frameLayout.getLocationOnScreen(position)
+            frameGlobalPosition = Offset(position[0].toFloat(), position[1].toFloat())
+
+            frameLayout.setContent(Recomposer.current()) {
+                Box(
+                    Modifier.fillMaxSize().onGloballyPositioned {
+                        realGlobalPosition = it.localToGlobal(localPosition)
+                        realLocalPosition = it.globalToLocal(
+                            framePadding + frameGlobalPosition!!
+                        )
+                        positionedLatch.countDown()
+                    }
+                )
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        assertThat(realGlobalPosition).isEqualTo(frameGlobalPosition!! + framePadding)
+        assertThat(realLocalPosition).isEqualTo(localPosition)
+    }
+
+    @Test
+    fun justAddedOnPositionedCallbackFiredWithoutLayoutChanges() {
+        val needCallback = mutableStateOf(false)
+
+        val positionedLatch = CountDownLatch(1)
+        rule.runOnUiThread {
+            activity.setContent {
+                val modifier = if (needCallback.value) {
+                    Modifier.onGloballyPositioned { positionedLatch.countDown() }
+                } else {
+                    Modifier
+                }
+                Box(modifier.fillMaxSize())
+            }
+        }
+
+        rule.runOnUiThread { needCallback.value = true }
+
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+    }
+
+    @Test
+    fun testRepositionTriggersCallback() {
+        val left = mutableStateOf(30)
+        var realLeft: Float? = null
+
+        var positionedLatch = CountDownLatch(1)
+        rule.runOnUiThread {
+            activity.setContent {
+                with(AmbientDensity.current) {
+                    Box {
+                        Box(
+                            Modifier.onGloballyPositioned {
+                                realLeft = it.positionInParent.x
+                                positionedLatch.countDown()
+                            }
+                                .fillMaxSize()
+                                .padding(start = left.value.toDp()),
+                        )
+                    }
+                }
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        positionedLatch = CountDownLatch(1)
+        rule.runOnUiThread { left.value = 40 }
+
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+        assertThat(realLeft).isEqualTo(40)
+    }
+
+    @Test
+    fun testGrandParentRepositionTriggersChildrenCallback() {
+        // when we reposition any parent layout is causes the change in global
+        // position of all the children down the tree(for example during the scrolling).
+        // children should be able to react on this change.
+        val left = mutableStateOf(20)
+        var realLeft: Float? = null
+        var positionedLatch = CountDownLatch(1)
+        rule.runOnUiThread {
+            activity.setContent {
+                with(AmbientDensity.current) {
+                    Box {
+                        Offset(left) {
+                            Box(Modifier.size(10.toDp())) {
+                                Box(Modifier.size(10.toDp())) {
+                                    Box(
+                                        Modifier.onGloballyPositioned {
+                                            realLeft = it.positionInRoot.x
+                                            positionedLatch.countDown()
+                                        }.size(10.toDp())
+                                    )
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+        positionedLatch = CountDownLatch(1)
+        rule.runOnUiThread { left.value = 40 }
+
+        assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+        assertThat(realLeft).isEqualTo(40)
+    }
+
+    @Test
+    fun testAlignmentLinesArePresent() {
+        val latch = CountDownLatch(1)
+        val line = VerticalAlignmentLine(::min)
+        val lineValue = 10
+        rule.runOnUiThread {
+            activity.setContent {
+                val onPositioned = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
+                    assertEquals(1, coordinates.providedAlignmentLines.size)
+                    assertEquals(lineValue, coordinates[line])
+                    latch.countDown()
+                }
+                Layout(modifier = onPositioned, content = { }) { _, _ ->
+                    layout(0, 0, mapOf(line to lineValue)) { }
+                }
+            }
+        }
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+    }
 }
 
 @Composable
@@ -449,4 +676,14 @@
             }
         }
     }
-}
\ No newline at end of file
+}
+
+@Composable
+private fun Offset(sizeModel: State<Int>, content: @Composable () -> Unit) {
+    // simple copy of Padding which doesn't recompose when the size changes
+    Layout(content) { measurables, constraints ->
+        layout(constraints.maxWidth, constraints.maxHeight) {
+            measurables.first().measure(constraints).placeRelative(sizeModel.value, 0)
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt
index 747973d..e9af374 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacedChildTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.layout
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.unit.Constraints
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -30,7 +29,6 @@
 class PlacedChildTest {
 
     @Test
-    @OptIn(ExperimentalLayoutNodeApi::class)
     fun remeasureNotPlacedChild() {
         val root = root {
             measureBlocks = UseChildSizeButNotPlace
@@ -59,7 +57,6 @@
     }
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 private val UseChildSizeButNotPlace = object : LayoutNode.NoIntrinsicsMeasureBlocks("") {
     override fun measure(
         measureScope: MeasureScope,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt
index 014540f..6dc67ef 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasurementModifierTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.layout
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -27,7 +26,6 @@
 @RunWith(AndroidJUnit4::class)
 class RemeasurementModifierTest {
     @Test
-    @OptIn(ExperimentalLayoutNodeApi::class)
     fun nodeIsRemeasuredAfterForceRemeasureBlocking() {
         var remeasurementObj: Remeasurement? = null
         val root = root {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt
index 14fbc4f..06f92fe 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RtlLayoutTest.kt
@@ -16,8 +16,13 @@
 
 package androidx.compose.ui.layout
 
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayout
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
+import androidx.compose.runtime.emptyContent
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.FixedSize
 import androidx.compose.ui.Modifier
@@ -31,6 +36,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import org.junit.Assert
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -191,6 +197,84 @@
         assertTrue(latch.await(1, TimeUnit.SECONDS))
         assertEquals(LayoutDirection.Ltr, actualDirection)
     }
+    @Test
+    fun testModifiedLayoutDirection_inMeasureScope() {
+        val latch = CountDownLatch(1)
+        val resultLayoutDirection = Ref<LayoutDirection>()
+
+        activityTestRule.runOnUiThread {
+            activity.setContent {
+                Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                    Layout(content = {}) { _, _ ->
+                        resultLayoutDirection.value = layoutDirection
+                        latch.countDown()
+                        layout(0, 0) {}
+                    }
+                }
+            }
+        }
+
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+        assertTrue(LayoutDirection.Rtl == resultLayoutDirection.value)
+    }
+
+    @Test
+    fun testModifiedLayoutDirection_inIntrinsicsMeasure() {
+        val latch = CountDownLatch(1)
+        var resultLayoutDirection: LayoutDirection? = null
+
+        activityTestRule.runOnUiThread {
+            activity.setContent {
+                @OptIn(ExperimentalLayout::class)
+                Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                    Layout(
+                        content = {},
+                        modifier = Modifier.preferredWidth(IntrinsicSize.Max),
+                        minIntrinsicWidthMeasureBlock = { _, _ -> 0 },
+                        minIntrinsicHeightMeasureBlock = { _, _ -> 0 },
+                        maxIntrinsicWidthMeasureBlock = { _, _ ->
+                            resultLayoutDirection = this.layoutDirection
+                            latch.countDown()
+                            0
+                        },
+                        maxIntrinsicHeightMeasureBlock = { _, _ -> 0 }
+                    ) { _, _ ->
+                        layout(0, 0) {}
+                    }
+                }
+            }
+        }
+
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+        Assert.assertNotNull(resultLayoutDirection)
+        assertTrue(LayoutDirection.Rtl == resultLayoutDirection)
+    }
+
+    @Test
+    fun testRestoreLocaleLayoutDirection() {
+        val latch = CountDownLatch(1)
+        val resultLayoutDirection = Ref<LayoutDirection>()
+
+        activityTestRule.runOnUiThread {
+            activity.setContent {
+                val initialLayoutDirection = AmbientLayoutDirection.current
+                Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+                    Box {
+                        Providers(AmbientLayoutDirection provides initialLayoutDirection) {
+                            Layout(emptyContent()) { _, _ ->
+                                resultLayoutDirection.value = layoutDirection
+                                latch.countDown()
+                                layout(0, 0) {}
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        assertTrue(latch.await(1, TimeUnit.SECONDS))
+        assertEquals(LayoutDirection.Ltr, resultLayoutDirection.value)
+    }
 
     @Composable
     private fun CustomLayout(
@@ -199,31 +283,10 @@
     ) {
         Providers(AmbientLayoutDirection provides testLayoutDirection) {
             Layout(
-                content = @Composable {
-                    FixedSize(
-                        size,
-                        modifier = Modifier.saveLayoutInfo(
-                            position[0],
-                            countDownLatch
-                        )
-                    ) {
-                    }
-                    FixedSize(
-                        size,
-                        modifier = Modifier.saveLayoutInfo(
-                            position[1],
-                            countDownLatch
-                        )
-                    ) {
-                    }
-                    FixedSize(
-                        size,
-                        modifier = Modifier.saveLayoutInfo(
-                            position[2],
-                            countDownLatch
-                        )
-                    ) {
-                    }
+                content = {
+                    FixedSize(size, modifier = Modifier.saveLayoutInfo(position[0], countDownLatch))
+                    FixedSize(size, modifier = Modifier.saveLayoutInfo(position[1], countDownLatch))
+                    FixedSize(size, modifier = Modifier.saveLayoutInfo(position[2], countDownLatch))
                 }
             ) { measurables, constraints ->
                 val placeables = measurables.map { it.measure(constraints) }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
index 6fb6d26..d20954b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
@@ -30,7 +30,7 @@
 import androidx.compose.ui.platform.AmbientContext
 import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.accessibilityLabel
+import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.assertLabelEquals
@@ -125,7 +125,7 @@
         var value = "First value"
 
         @Composable fun semanticsNode(text: String, id: Int) {
-            Box(Modifier.testTag("text$id").semantics { accessibilityLabel = text }) {
+            Box(Modifier.testTag("text$id").semantics { contentDescription = text }) {
             }
         }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
index 84b5920..04dca9c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
@@ -59,12 +59,10 @@
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutEmitHelper
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.node.Ref
-import androidx.compose.ui.node.isAttached
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.captureToImage
@@ -573,7 +571,7 @@
     }
 
     @Test
-    @OptIn(ExperimentalLayoutNodeApi::class, ExperimentalComposeApi::class)
+    @OptIn(ExperimentalComposeApi::class)
     fun testComposeInsideView_attachingAndDetaching() {
         var composeContent by mutableStateOf(true)
         var node: LayoutNode? = null
@@ -585,7 +583,7 @@
                             emit<LayoutNode, Applier<Any>>(
                                 ctor = LayoutEmitHelper.constructor,
                                 update = {
-                                    node = this.node
+                                    set(Unit) { node = this }
                                     set(noOpMeasureBlocks, LayoutEmitHelper.setMeasureBlocks)
                                 }
                             )
@@ -620,7 +618,7 @@
         assertNotNull(innerAndroidComposeView)
         assertTrue(innerAndroidComposeView!!.isAttachedToWindow)
         assertNotNull(node)
-        assertTrue(node!!.isAttached())
+        assertTrue(node!!.isAttached)
 
         rule.runOnIdle { composeContent = false }
 
@@ -628,7 +626,7 @@
         rule.runOnIdle {
             assertFalse(innerAndroidComposeView!!.isAttachedToWindow)
             // the node stays attached after the compose view is detached
-            assertTrue(node!!.isAttached())
+            assertTrue(node!!.isAttached)
         }
     }
 
@@ -783,7 +781,6 @@
         }
     }
 
-    @OptIn(ExperimentalLayoutNodeApi::class)
     private val noOpMeasureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks("") {
         override fun measure(
             measureScope: MeasureScope,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt
index c08af79..f82dba7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/DepthSortedSetTest.kt
@@ -17,7 +17,6 @@
 package androidx.compose.ui.platform
 
 import androidx.compose.ui.node.DepthSortedSet
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.node.add
@@ -31,7 +30,6 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalLayoutNodeApi::class)
 class DepthSortedSetTest {
 
     @Test
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
index f189a3d..d50dc78 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.window.Dialog
 import androidx.compose.ui.window.Popup
@@ -33,7 +32,6 @@
 import java.util.concurrent.TimeUnit.SECONDS
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class WindowManagerAmbientTest {
     @get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt
index a0d27e1..5182721 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WrapperTest.kt
@@ -176,6 +176,7 @@
         activityScenario.onActivity {
             var composed = false
             it.setContent {
+                check(!composed) { "the content is expected to be composed once" }
                 composed = true
             }
             assertTrue("setContent didn't compose the content synchronously", composed)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index c7136d0..97c6889 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -19,7 +19,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.InspectableValue
@@ -31,12 +30,14 @@
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertCountEquals
 import androidx.compose.ui.test.assertLabelEquals
+import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.assertValueEquals
 import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onAllNodesWithLabel
+import androidx.compose.ui.test.onAllNodesWithContentDescription
 import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithLabel
+import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.AnnotatedString
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -83,7 +84,7 @@
         val recomposeForcer = mutableStateOf(0)
         rule.setContent {
             recomposeForcer.value
-            CountingLayout(Modifier.semantics { accessibilityLabel = "label" }, layoutCounter)
+            CountingLayout(Modifier.semantics { contentDescription = "label" }, layoutCounter)
         }
 
         rule.runOnIdle { assertEquals(1, layoutCounter.count) }
@@ -103,13 +104,13 @@
         rule.setContent {
             SimpleTestLayout(
                 Modifier.testTag(TestTag)
-                    .semantics(mergeDescendants = true) { accessibilityLabel = root }
+                    .semantics(mergeDescendants = true) { contentDescription = root }
             ) {
-                SimpleTestLayout(Modifier.semantics { accessibilityLabel = child1 }) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = grandchild1 }) { }
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = grandchild2 }) { }
+                SimpleTestLayout(Modifier.semantics { contentDescription = child1 }) {
+                    SimpleTestLayout(Modifier.semantics { contentDescription = grandchild1 }) { }
+                    SimpleTestLayout(Modifier.semantics { contentDescription = grandchild2 }) { }
                 }
-                SimpleTestLayout(Modifier.semantics { accessibilityLabel = child2 }) { }
+                SimpleTestLayout(Modifier.semantics { contentDescription = child2 }) { }
             }
         }
 
@@ -126,9 +127,9 @@
         val label2 = "bar"
         rule.setContent {
             SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag1)) {
-                SimpleTestLayout(Modifier.semantics { accessibilityLabel = label1 }) { }
+                SimpleTestLayout(Modifier.semantics { contentDescription = label1 }) { }
                 SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag2)) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = label2 }) { }
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
                 }
             }
         }
@@ -138,13 +139,41 @@
     }
 
     @Test
+    fun clearAndSetSemantics() {
+        val tag1 = "tag1"
+        val tag2 = "tag2"
+        val label1 = "foo"
+        val label2 = "hidden"
+        val label3 = "baz"
+        rule.setContent {
+            SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag1)) {
+                SimpleTestLayout(Modifier.semantics { contentDescription = label1 }) { }
+                SimpleTestLayout(Modifier.clearAndSetSemantics {}) {
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
+                }
+                SimpleTestLayout(Modifier.clearAndSetSemantics { contentDescription = label3 }) {
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
+                }
+                SimpleTestLayout(
+                    Modifier.semantics(mergeDescendants = true) {}.testTag(tag2)
+                        .clearAndSetSemantics { text = AnnotatedString(label1) }
+                ) {
+                    SimpleTestLayout(Modifier.semantics { text = AnnotatedString(label2) }) { }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag1).assertLabelEquals("$label1, $label3")
+        rule.onNodeWithTag(tag2).assertTextEquals(label1)
+    }
+    @Test
     fun removingMergedSubtree_updatesSemantics() {
         val label = "foo"
         val showSubtree = mutableStateOf(true)
         rule.setContent {
             SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(TestTag)) {
                 if (showSubtree.value) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
                 }
             }
         }
@@ -154,7 +183,7 @@
         rule.runOnIdle { showSubtree.value = false }
 
         rule.onNodeWithTag(TestTag)
-            .assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
+            .assertDoesNotHaveProperty(SemanticsProperties.ContentDescription)
 
         rule.onAllNodesWithText(label).assertCountEquals(0)
     }
@@ -166,16 +195,16 @@
         val showNewNode = mutableStateOf(false)
         rule.setContent {
             SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(TestTag)) {
-                SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+                SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
                 if (showNewNode.value) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityValue = value }) { }
+                    SimpleTestLayout(Modifier.semantics { stateDescription = value }) { }
                 }
             }
         }
 
         rule.onNodeWithTag(TestTag)
             .assertLabelEquals(label)
-            .assertDoesNotHaveProperty(SemanticsProperties.AccessibilityValue)
+            .assertDoesNotHaveProperty(SemanticsProperties.StateDescription)
 
         rule.runOnIdle { showNewNode.value = true }
 
@@ -191,18 +220,18 @@
         rule.setContent {
             SimpleTestLayout(Modifier.testTag(TestTag)) {
                 if (showSubtree.value) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
                 }
             }
         }
 
-        rule.onAllNodesWithLabel(label).assertCountEquals(1)
+        rule.onAllNodesWithContentDescription(label).assertCountEquals(1)
 
         rule.runOnIdle {
             showSubtree.value = false
         }
 
-        rule.onAllNodesWithLabel(label).assertCountEquals(0)
+        rule.onAllNodesWithContentDescription(label).assertCountEquals(0)
     }
 
     @Test
@@ -213,7 +242,7 @@
         rule.setContent {
             SimpleTestLayout(
                 Modifier.testTag(TestTag).semantics {
-                    accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                    contentDescription = if (isAfter.value) afterLabel else beforeLabel
                 }
             ) {}
         }
@@ -235,7 +264,7 @@
             SimpleTestLayout(Modifier.testTag("don't care")) {
                 SimpleTestLayout(
                     Modifier.testTag(TestTag).semantics {
-                        accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                        contentDescription = if (isAfter.value) afterLabel else beforeLabel
                     }
                 ) {}
             }
@@ -259,7 +288,7 @@
                 SimpleTestLayout {
                     SimpleTestLayout(
                         Modifier.testTag(TestTag).semantics {
-                            accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                            contentDescription = if (isAfter.value) afterLabel else beforeLabel
                         }
                     ) {}
                 }
@@ -283,7 +312,7 @@
             SimpleTestLayout(Modifier.testTag(TestTag).semantics(mergeDescendants = true) {}) {
                 SimpleTestLayout(
                     Modifier.semantics {
-                        accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                        contentDescription = if (isAfter.value) afterLabel else beforeLabel
                     }
                 ) {}
             }
@@ -302,14 +331,14 @@
         rule.setContent {
             SimpleTestLayout(Modifier.testTag(TestTag)) {
                 SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
                 }
             }
         }
 
         rule.onNodeWithTag(TestTag)
-            .assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
-        rule.onNodeWithLabel(label) // assert exists
+            .assertDoesNotHaveProperty(SemanticsProperties.ContentDescription)
+        rule.onNodeWithContentDescription(label) // assert exists
     }
 
     @Test
@@ -324,7 +353,7 @@
         rule.setContent {
             SimpleTestLayout(
                 Modifier.testTag(TestTag).semantics {
-                    accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                    contentDescription = if (isAfter.value) afterLabel else beforeLabel
                 }
             ) {
                 SimpleTestLayout(Modifier.semantics { }) { }
@@ -355,7 +384,7 @@
         rule.setContent {
             SimpleTestLayout(
                 Modifier.testTag(TestTag).semantics {
-                    accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                    contentDescription = if (isAfter.value) afterLabel else beforeLabel
                     onClick(
                         action = {
                             if (isAfter.value) afterAction() else beforeAction()
@@ -365,7 +394,7 @@
                 }
             ) {
                 SimpleTestLayout {
-                    remember { nodeCount++ }
+                    nodeCount++
                 }
             }
         }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
index 03c512c..5b07d5e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
@@ -20,7 +20,6 @@
 import android.view.View
 import android.widget.EditText
 import androidx.activity.ComponentActivity
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.platform.AmbientView
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.text.InternalTextApi
@@ -32,7 +31,7 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class, InternalTextApi::class)
+@OptIn(InternalTextApi::class)
 @RunWith(AndroidJUnit4::class)
 class EditTextInteropTest {
     @get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
index ee36bdc..dcdc42e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
@@ -26,7 +26,6 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.test.isDialog
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.unit.dp
@@ -74,7 +73,7 @@
     @Test
     fun forcedFlagOnDialogToDisabled() {
         rule.setContent {
-            TestDialog(AndroidDialogProperties(SecureFlagPolicy.SecureOff))
+            TestDialog(AndroidDialogProperties(securePolicy = SecureFlagPolicy.SecureOff))
         }
 
         // This tests that we also override the flag from the Activity
@@ -84,7 +83,7 @@
     @Test
     fun forcedFlagOnDialogToEnabled() {
         rule.setContent {
-            TestDialog(AndroidDialogProperties(SecureFlagPolicy.SecureOn))
+            TestDialog(AndroidDialogProperties(securePolicy = SecureFlagPolicy.SecureOn))
         }
 
         assertThat(isSecureFlagEnabledForDialog()).isTrue()
@@ -93,7 +92,7 @@
     @Test
     fun toggleFlagOnDialog() {
         var properties: AndroidDialogProperties?
-        by mutableStateOf(AndroidDialogProperties(SecureFlagPolicy.SecureOff))
+        by mutableStateOf(AndroidDialogProperties(securePolicy = SecureFlagPolicy.SecureOff))
 
         rule.setContent {
             TestDialog(properties)
@@ -102,11 +101,11 @@
         assertThat(isSecureFlagEnabledForDialog()).isFalse()
 
         // Toggle flag
-        properties = AndroidDialogProperties(SecureFlagPolicy.SecureOn)
+        properties = AndroidDialogProperties(securePolicy = SecureFlagPolicy.SecureOn)
         assertThat(isSecureFlagEnabledForDialog()).isTrue()
 
         // Set to inherit
-        properties = AndroidDialogProperties(SecureFlagPolicy.Inherit)
+        properties = AndroidDialogProperties(securePolicy = SecureFlagPolicy.Inherit)
         assertThat(isSecureFlagEnabledForDialog()).isEqualTo(setSecureFlagOnActivity)
     }
 
@@ -123,10 +122,9 @@
     }
 
     private fun isSecureFlagEnabledForDialog(): Boolean {
-        @OptIn(ExperimentalLayoutNodeApi::class)
         val owner = rule
             .onNode(isDialog())
-            .fetchSemanticsNode("").componentNode.owner as View
+            .fetchSemanticsNode("").owner as View
         return (owner.rootView.layoutParams as WindowManager.LayoutParams).flags and
             WindowManager.LayoutParams.FLAG_SECURE != 0
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
index 5bbf8203..72d4466 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
@@ -33,7 +33,6 @@
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import androidx.test.uiautomator.UiDevice
 import org.junit.Assert.assertEquals
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -148,6 +147,34 @@
     }
 
     @Test
+    fun dialogTest_isNotDismissed_whenDismissOnClickOutsideIsFalse() {
+        rule.setContent {
+            val showDialog = remember { mutableStateOf(true) }
+
+            if (showDialog.value) {
+                Dialog(
+                    onDismissRequest = {
+                        showDialog.value = false
+                    },
+                    properties = AndroidDialogProperties(dismissOnClickOutside = false)
+                ) {
+                    BasicText(defaultText)
+                }
+            }
+        }
+
+        rule.onNodeWithText(defaultText).assertIsDisplayed()
+
+        // Click outside the dialog to try to dismiss it
+        val outsideX = 0
+        val outsideY = rule.displaySize.height / 2
+        UiDevice.getInstance(getInstrumentation()).click(outsideX, outsideY)
+
+        // The Dialog should still be visible
+        rule.onNodeWithText(defaultText).assertIsDisplayed()
+    }
+
+    @Test
     fun dialogTest_isDismissed_whenSpecified_backButtonPressed() {
         rule.setContent {
             val showDialog = remember { mutableStateOf(true) }
@@ -171,9 +198,6 @@
         rule.onNodeWithText(defaultText).assertDoesNotExist()
     }
 
-    // TODO(pavlis): Espresso loses focus on the dialog after back press. That makes the
-    // subsequent query to fails.
-    @Ignore
     @Test
     fun dialogTest_isNotDismissed_whenNotSpecified_backButtonPressed() {
         rule.setContent {
@@ -196,6 +220,32 @@
     }
 
     @Test
+    fun dialogTest_isNotDismissed_whenDismissOnBackPressIsFalse() {
+        rule.setContent {
+            val showDialog = remember { mutableStateOf(true) }
+
+            if (showDialog.value) {
+                Dialog(
+                    onDismissRequest = {
+                        showDialog.value = false
+                    },
+                    properties = AndroidDialogProperties(dismissOnBackPress = false)
+                ) {
+                    BasicText(defaultText)
+                }
+            }
+        }
+
+        rule.onNodeWithText(defaultText).assertIsDisplayed()
+
+        // Click the back button to try to dismiss the dialog
+        Espresso.pressBack()
+
+        // The Dialog should still be visible
+        rule.onNodeWithText(defaultText).assertIsDisplayed()
+    }
+
+    @Test
     fun dialog_preservesAmbients() {
         val ambient = ambientOf<Float>()
         var value = 0f
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt
index 2b9b545..9c57132 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt
@@ -24,6 +24,7 @@
 import android.view.autofill.AutofillManager
 import android.view.autofill.AutofillValue
 import androidx.annotation.RequiresApi
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.graphics.toAndroidRect
 
 /**
@@ -32,6 +33,7 @@
  * @param view The parent compose view.
  * @param autofillTree The autofill tree. This will be replaced by a semantic tree (b/138604305).
  */
+@ExperimentalComposeUiApi
 @RequiresApi(Build.VERSION_CODES.O)
 internal class AndroidAutofill(val view: View, val autofillTree: AutofillTree) : Autofill {
 
@@ -63,6 +65,7 @@
  *
  * This function populates the view structure using the information in the { AutofillTree }.
  */
+@ExperimentalComposeUiApi
 @RequiresApi(Build.VERSION_CODES.O)
 internal fun AndroidAutofill.populateViewStructure(root: ViewStructure) {
 
@@ -96,6 +99,7 @@
 /**
  * Triggers onFill() in response to a request from the autofill framework.
  */
+@ExperimentalComposeUiApi
 @RequiresApi(Build.VERSION_CODES.O)
 internal fun AndroidAutofill.performAutofill(values: SparseArray<AutofillValue>) {
     for (index in 0 until values.size()) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt
index 6d50b7e..839587ce 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt
@@ -21,6 +21,7 @@
 import android.view.View
 import android.view.autofill.AutofillManager
 import androidx.annotation.RequiresApi
+import androidx.compose.ui.ExperimentalComposeUiApi
 
 /**
  * Autofill Manager callback.
@@ -58,6 +59,7 @@
 /**
  * Registers the autofill debug callback.
  */
+@ExperimentalComposeUiApi
 @RequiresApi(Build.VERSION_CODES.O)
 internal fun AndroidAutofill.registerCallback() {
     autofillManager.registerCallback(AutofillCallback)
@@ -66,6 +68,7 @@
 /**
  * Unregisters the autofill debug callback.
  */
+@ExperimentalComposeUiApi
 @RequiresApi(Build.VERSION_CODES.O)
 internal fun AndroidAutofill.unregisterCallback() {
     autofillManager.unregisterCallback(AutofillCallback)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt
index 5e057b5..94a5b9a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt
@@ -52,6 +52,7 @@
 import androidx.autofill.HintConstants.AUTOFILL_HINT_POSTAL_CODE
 import androidx.autofill.HintConstants.AUTOFILL_HINT_SMS_OTP
 import androidx.autofill.HintConstants.AUTOFILL_HINT_USERNAME
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
 import androidx.compose.ui.autofill.AutofillType.AddressCountry
 import androidx.compose.ui.autofill.AutofillType.AddressLocality
@@ -93,6 +94,7 @@
  * Gets the Android specific [AutofillHint][android.view.ViewStructure.setAutofillHints]
  * corresponding to the current [AutofillType].
  */
+@ExperimentalComposeUiApi
 internal val AutofillType.androidType: String
     get() {
         val androidAutofillType = androidAutofillTypes[this]
@@ -103,6 +105,7 @@
 /**
  * Maps each [AutofillType] to one of the  autofill hints in [androidx.autofill.HintConstants]
  */
+@ExperimentalComposeUiApi
 private val androidAutofillTypes: HashMap<AutofillType, String> = hashMapOf(
     EmailAddress to AUTOFILL_HINT_EMAIL_ADDRESS,
     Username to AUTOFILL_HINT_USERNAME,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt
index b5a86d7..1d4d09b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt
@@ -18,15 +18,11 @@
 
 import android.view.KeyEvent.ACTION_DOWN
 import android.view.KeyEvent.ACTION_UP
-import android.view.KeyEvent.META_ALT_LEFT_ON
-import android.view.KeyEvent.META_ALT_MASK
-import android.view.KeyEvent.META_ALT_RIGHT_ON
 import androidx.compose.ui.input.key.KeyEventType.KeyDown
 import androidx.compose.ui.input.key.KeyEventType.KeyUp
 import androidx.compose.ui.input.key.KeyEventType.Unknown
 import android.view.KeyEvent as AndroidKeyEvent
 
-@OptIn(ExperimentalKeyInput::class)
 internal inline class KeyEventAndroid(val keyEvent: AndroidKeyEvent) : KeyEvent {
 
     override val key: Key
@@ -52,25 +48,4 @@
 
     override val isShiftPressed: Boolean
         get() = keyEvent.isShiftPressed
-
-    @Suppress("DEPRECATION", "OverridingDeprecatedMember")
-    override val alt: Alt
-        get() = AltAndroid(keyEvent)
-}
-
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalKeyInput::class)
-internal inline class AltAndroid(val keyEvent: AndroidKeyEvent) : Alt {
-    override val isLeftAltPressed
-        get() = (keyEvent.metaState and META_ALT_LEFT_ON) != 0
-
-    override val isRightAltPressed
-        get() = (keyEvent.metaState and META_ALT_RIGHT_ON) != 0
-
-    /**
-     * We override [isPressed] because Android has some synthetic meta states (eg. META_ALT_LOCKED)
-     * and provides a META_ALT_MASK that can be used to check if the Alt key is pressed.
-     */
-    override val isPressed
-        get() = (keyEvent.metaState and META_ALT_MASK) != 0
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt
index 144631b..92a0968 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt
@@ -19,112 +19,73 @@
 
 import android.view.View
 import android.view.ViewGroup
-import androidx.compose.runtime.Applier
+import androidx.compose.runtime.AbstractApplier
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.viewinterop.AndroidViewHolder
 import androidx.compose.ui.viewinterop.InternalInteropApi
 import androidx.compose.ui.viewinterop.ViewBlockHolder
 
-// TODO: evaluate if this class is necessary or not
-private class Stack<T> {
-    private val backing = ArrayList<T>()
-
-    val size: Int get() = backing.size
-
-    fun push(value: T) = backing.add(value)
-    fun pop(): T = backing.removeAt(size - 1)
-    fun peek(): T = backing[size - 1]
-    fun isEmpty() = backing.isEmpty()
-    fun isNotEmpty() = !isEmpty()
-    fun clear() = backing.clear()
-}
-
-@OptIn(ExperimentalLayoutNodeApi::class)
-class UiApplier(
-    private val root: Any
-) : Applier<Any> {
-    private val stack = Stack<Any>()
-    private data class PendingInsert(val index: Int, val instance: Any)
-    // TODO(b/159073250): remove
-    private val pendingInserts = Stack<PendingInsert>()
-
+class UiApplier(root: Any) : AbstractApplier<Any>(root) {
     private fun invalidNode(node: Any): Nothing =
         error("Unsupported node type ${node.javaClass.simpleName}")
 
-    override var current: Any = root
-        private set
-
-    override fun down(node: Any) {
-        stack.push(current)
-        current = node
-    }
-
     override fun up() {
         val instance = current
-        val parent = stack.pop()
-        current = parent
-        // TODO(lmr): We should strongly consider removing this ViewAdapter concept
+        super.up()
+        val parent = current
+        if (parent is ViewGroup && instance is View) {
+            instance.getViewAdapterIfExists()?.didUpdate(instance, parent)
+        }
+    }
+
+    override fun insertTopDown(index: Int, instance: Any) {
+        // Ignored. Insert is performed in [insertBottomUp] to build the tree bottom-up to avoid
+        // duplicate notification when the child nodes enter the tree.
+    }
+
+    override fun insertBottomUp(index: Int, instance: Any) {
         val adapter = when (instance) {
             is View -> instance.getViewAdapterIfExists()
             else -> null
         }
-        if (pendingInserts.isNotEmpty()) {
-            val pendingInsert = pendingInserts.peek()
-            if (pendingInsert.instance == instance) {
-                val index = pendingInsert.index
-                pendingInserts.pop()
-                when (parent) {
-                    is ViewGroup ->
-                        when (instance) {
-                            is View -> {
-                                adapter?.willInsert(instance, parent)
-                                parent.addView(instance, index)
-                                adapter?.didInsert(instance, parent)
-                            }
-                            is LayoutNode -> {
-                                val composeView = AndroidComposeView(parent.context)
-                                parent.addView(composeView, index)
-                                composeView.root.insertAt(0, instance)
-                            }
-                            else -> invalidNode(instance)
-                        }
-                    is LayoutNode ->
-                        when (instance) {
-                            is View -> {
-                                // Wrap the instance in an AndroidViewHolder, unless the instance
-                                // itself is already one.
-                                @OptIn(InternalInteropApi::class)
-                                val androidViewHolder =
-                                    if (instance is AndroidViewHolder) {
-                                        instance
-                                    } else {
-                                        ViewBlockHolder<View>(instance.context).apply {
-                                            view = instance
-                                        }
-                                    }
-
-                                parent.insertAt(index, androidViewHolder.toLayoutNode())
-                            }
-                            is LayoutNode -> parent.insertAt(index, instance)
-                            else -> invalidNode(instance)
-                        }
-                    else -> invalidNode(parent)
+        when (val parent = current) {
+            is ViewGroup ->
+                when (instance) {
+                    is View -> {
+                        adapter?.willInsert(instance, parent)
+                        parent.addView(instance, index)
+                        adapter?.didInsert(instance, parent)
+                    }
+                    is LayoutNode -> {
+                        val composeView = AndroidComposeView(parent.context)
+                        parent.addView(composeView, index)
+                        composeView.root.insertAt(0, instance)
+                    }
+                    else -> invalidNode(instance)
                 }
-                return
-            }
-        }
-        if (parent is ViewGroup)
-            adapter?.didUpdate(instance as View, parent)
-    }
+            is LayoutNode ->
+                when (instance) {
+                    is View -> {
+                        // Wrap the instance in an AndroidViewHolder, unless the instance
+                        // itself is already one.
+                        @OptIn(InternalInteropApi::class)
+                        val androidViewHolder =
+                            if (instance is AndroidViewHolder) {
+                                instance
+                            } else {
+                                ViewBlockHolder<View>(instance.context).apply {
+                                    view = instance
+                                }
+                            }
 
-    override fun insert(index: Int, instance: Any) {
-        pendingInserts.push(
-            PendingInsert(
-                index,
-                instance
-            )
-        )
+                        parent.insertAt(index, androidViewHolder.toLayoutNode())
+                    }
+                    is LayoutNode -> parent.insertAt(index, instance)
+                    else -> invalidNode(instance)
+                }
+            else -> invalidNode(parent)
+        }
     }
 
     override fun remove(index: Int, count: Int) {
@@ -163,13 +124,31 @@
         }
     }
 
-    override fun clear() {
-        stack.clear()
-        current = root
+    override fun onClear() {
         when (root) {
             is ViewGroup -> root.removeAllViews()
             is LayoutNode -> root.removeAll()
             else -> invalidNode(root)
         }
     }
+
+    override fun onEndChanges() {
+        super.onEndChanges()
+        if (root is ViewGroup) {
+            clearInvalidObservations(root)
+        } else if (root is LayoutNode) {
+            (root.owner as? AndroidComposeView)?.clearInvalidObservations()
+        }
+    }
+
+    private fun clearInvalidObservations(viewGroup: ViewGroup) {
+        for (i in 0 until viewGroup.childCount) {
+            val child = viewGroup.getChildAt(i)
+            if (child is AndroidComposeView) {
+                child.clearInvalidObservations()
+            } else if (child is ViewGroup) {
+                clearInvalidObservations(child)
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
index ab6bb3d..45698e4 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
@@ -95,7 +95,7 @@
  * Builds a [LayoutNode] tree representation for an Android [View].
  * The component nodes will proxy the Compose core calls to the [View].
  */
-@OptIn(ExperimentalLayoutNodeApi::class, InternalInteropApi::class)
+@OptIn(InternalInteropApi::class)
 internal fun AndroidViewHolder.toLayoutNode(): LayoutNode {
     // TODO(soboleva): add layout direction here?
     // TODO(popam): forward pointer input, accessibility, focus
@@ -106,7 +106,7 @@
         .pointerInteropFilter(this)
         .drawBehind {
             drawIntoCanvas { canvas ->
-                (layoutNode.owner as? AndroidOwner)
+                (layoutNode.owner as? AndroidComposeView)
                     ?.drawAndroidView(this@toLayoutNode, canvas.nativeCanvas)
             }
         }.onGloballyPositioned {
@@ -122,11 +122,11 @@
 
     var viewRemovedOnDetach: View? = null
     layoutNode.onAttach = { owner ->
-        (owner as? AndroidOwner)?.addAndroidView(this, layoutNode)
+        (owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)
         if (viewRemovedOnDetach != null) view = viewRemovedOnDetach
     }
     layoutNode.onDetach = { owner ->
-        (owner as? AndroidOwner)?.removeAndroidView(this)
+        (owner as? AndroidComposeView)?.removeAndroidView(this)
         viewRemovedOnDetach = view
         view = null
     }
@@ -168,7 +168,6 @@
     return layoutNode
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 private fun View.layoutAccordingTo(layoutNode: LayoutNode) {
     val position = layoutNode.coordinates.positionInRoot
     val x = position.x.roundToInt()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt
index ed51935..a102b98 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt
@@ -28,7 +28,7 @@
  * This class contains the implementation of text segment iterators
  * for accessibility support.
  *
- * Note: We want to be able to iterator over [SemanticsProperties.AccessibilityLabel] of any
+ * Note: We want to be able to iterator over [SemanticsProperties.ContentDescription] of any
  * component.
  */
 internal class AccessibilityIterators {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt
index 9e28215..083e828 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt
@@ -34,6 +34,7 @@
 import androidx.compose.runtime.savedinstancestate.AmbientUiSavedStateRegistry
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticAmbientOf
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelStoreOwner
 
@@ -133,9 +134,9 @@
 val AmbientViewModelStoreOwner = staticAmbientOf<ViewModelStoreOwner>()
 
 @Composable
-@OptIn(InternalAnimationApi::class)
-internal fun ProvideAndroidAmbients(owner: AndroidOwner, content: @Composable () -> Unit) {
-    val view = owner.view
+@OptIn(ExperimentalComposeUiApi::class, InternalAnimationApi::class)
+internal fun ProvideAndroidAmbients(owner: AndroidComposeView, content: @Composable () -> Unit) {
+    val view = owner
     val context = view.context
     val scope = rememberCoroutineScope()
     val rootAnimationClock = remember(scope) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index 50e46ae..794fcbb 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -35,6 +35,7 @@
 import android.view.inputmethod.InputConnection
 import androidx.annotation.RequiresApi
 import androidx.compose.runtime.ExperimentalComposeApi
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.autofill.AndroidAutofill
 import androidx.compose.ui.autofill.Autofill
@@ -43,7 +44,6 @@
 import androidx.compose.ui.autofill.populateViewStructure
 import androidx.compose.ui.autofill.registerCallback
 import androidx.compose.ui.autofill.unregisterCallback
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FOCUS_TAG
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusManagerImpl
@@ -51,7 +51,6 @@
 import androidx.compose.ui.graphics.CanvasHolder
 import androidx.compose.ui.hapticfeedback.AndroidHapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventAndroid
 import androidx.compose.ui.input.key.KeyInputModifier
@@ -59,12 +58,12 @@
 import androidx.compose.ui.input.pointer.PointerInputEventProcessor
 import androidx.compose.ui.input.pointer.ProcessResult
 import androidx.compose.ui.layout.RootMeasureBlocks
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.UsageByParent
 import androidx.compose.ui.node.MeasureAndLayoutDelegate
 import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.node.Owner
 import androidx.compose.ui.node.OwnerSnapshotObserver
 import androidx.compose.ui.semantics.SemanticsModifierCore
 import androidx.compose.ui.semantics.SemanticsOwner
@@ -81,21 +80,24 @@
 import androidx.compose.ui.viewinterop.InternalInteropApi
 import androidx.core.os.HandlerCompat
 import androidx.core.view.ViewCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelStoreOwner
 import androidx.lifecycle.ViewTreeLifecycleOwner
 import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.savedstate.SavedStateRegistryOwner
 import androidx.savedstate.ViewTreeSavedStateRegistryOwner
 import java.lang.reflect.Method
 import android.view.KeyEvent as AndroidKeyEvent
 
-@SuppressLint("ViewConstructor")
+@SuppressLint("ViewConstructor", "VisibleForTests")
 @OptIn(
     ExperimentalComposeApi::class,
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class,
-    ExperimentalLayoutNodeApi::class
+    ExperimentalComposeUiApi::class,
 )
 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal class AndroidComposeView(context: Context) : ViewGroup(context), AndroidOwner {
+internal class AndroidComposeView(context: Context) :
+    ViewGroup(context), Owner, ViewRootForTest {
 
     override val view: View = this
 
@@ -105,6 +107,7 @@
     private val semanticsModifier = SemanticsModifierCore(
         id = SemanticsModifierCore.generateSemanticsId(),
         mergeDescendants = false,
+        clearAndSetSemantics = false,
         properties = {}
     )
 
@@ -146,10 +149,12 @@
     // TODO(mount): reinstate when coroutines are supported by IR compiler
     // private val ownerScope = CoroutineScope(Dispatchers.Main.immediate + Job())
 
-    // Used for updating the ConfigurationAmbient when configuration changes - consume the
-    // configuration ambient instead of changing this observer if you are writing a component that
-    // adapts to configuration changes.
-    override var configurationChangeObserver: (Configuration) -> Unit = {}
+    /**
+     * Used for updating the ConfigurationAmbient when configuration changes - consume the
+     * configuration ambient instead of changing this observer if you are writing a component
+     * that adapts to configuration changes.
+     */
+    var configurationChangeObserver: (Configuration) -> Unit = {}
 
     private val _autofill = if (autofillSupported()) AndroidAutofill(this, autofillTree) else null
 
@@ -174,13 +179,6 @@
     @OptIn(InternalCoreApi::class)
     override var showLayoutBounds = false
 
-    private val clearInvalidObservations: Runnable = Runnable {
-        if (observationClearRequested) {
-            observationClearRequested = false
-            snapshotObserver.clearInvalidObservations()
-        }
-    }
-
     private var _androidViewsHandler: AndroidViewsHandler? = null
     private val androidViewsHandler: AndroidViewsHandler
         get() {
@@ -229,10 +227,14 @@
     // so that we don't have to continue using try/catch after fails once.
     private var isRenderNodeCompatible = true
 
-    override var viewTreeOwners: AndroidOwner.ViewTreeOwners? = null
+    /**
+     * Current [ViewTreeOwners]. Use [setOnViewTreeOwnersAvailable] if you want to
+     * execute your code when the object will be created.
+     */
+    var viewTreeOwners: ViewTreeOwners? = null
         private set
 
-    private var onViewTreeOwnersAvailable: ((AndroidOwner.ViewTreeOwners) -> Unit)? = null
+    private var onViewTreeOwnersAvailable: ((ViewTreeOwners) -> Unit)? = null
 
     // executed when the layout pass has been finished. as a result of it our view could be moved
     // inside the window (we are interested not only in the event when our parent positioned us
@@ -283,7 +285,7 @@
         isFocusableInTouchMode = true
         clipChildren = false
         ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate)
-        AndroidOwner.onAndroidOwnerCreatedCallback?.invoke(this)
+        ViewRootForTest.onViewCreatedCallback?.invoke(this)
         root.attach(this)
     }
 
@@ -325,27 +327,56 @@
     }
 
     fun requestClearInvalidObservations() {
-        val handler = handler
-        if (!observationClearRequested && handler != null) {
-            observationClearRequested = true
-            handler.postAtFrontOfQueue(clearInvalidObservations)
+        observationClearRequested = true
+    }
+
+    internal fun clearInvalidObservations() {
+        if (observationClearRequested) {
+            snapshotObserver.clearInvalidObservations()
+            observationClearRequested = false
+        }
+        val childAndroidViews = _androidViewsHandler
+        if (childAndroidViews != null) {
+            clearChildInvalidObservations(childAndroidViews)
         }
     }
 
+    private fun clearChildInvalidObservations(viewGroup: ViewGroup) {
+        for (i in 0 until viewGroup.childCount) {
+            val child = viewGroup.getChildAt(i)
+            if (child is AndroidComposeView) {
+                child.clearInvalidObservations()
+            } else if (child is ViewGroup) {
+                clearChildInvalidObservations(child)
+            }
+        }
+    }
+
+    /**
+     * Called to inform the owner that a new Android [View] was [attached][Owner.onAttach]
+     * to the hierarchy.
+     */
     @OptIn(InternalInteropApi::class)
-    override fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
+    fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
         androidViewsHandler.layoutNode[view] = layoutNode
         androidViewsHandler.addView(view)
     }
 
+    /**
+     * Called to inform the owner that an Android [View] was [detached][Owner.onDetach]
+     * from the hierarchy.
+     */
     @OptIn(InternalInteropApi::class)
-    override fun removeAndroidView(view: AndroidViewHolder) {
+    fun removeAndroidView(view: AndroidViewHolder) {
         androidViewsHandler.removeView(view)
         androidViewsHandler.layoutNode.remove(view)
     }
 
+    /**
+     * Called to ask the owner to draw a child Android [View] to [canvas].
+     */
     @OptIn(InternalInteropApi::class)
-    override fun drawAndroidView(view: AndroidViewHolder, canvas: android.graphics.Canvas) {
+    fun drawAndroidView(view: AndroidViewHolder, canvas: android.graphics.Canvas) {
         androidViewsHandler.drawView(view, canvas)
     }
 
@@ -504,7 +535,11 @@
         }
     }
 
-    override fun setOnViewTreeOwnersAvailable(callback: (AndroidOwner.ViewTreeOwners) -> Unit) {
+    /**
+     * The callback to be executed when [viewTreeOwners] is created and not-null anymore.
+     * Note that this callback will be fired inline when it is already available
+     */
+    fun setOnViewTreeOwnersAvailable(callback: (ViewTreeOwners) -> Unit) {
         val viewTreeOwners = viewTreeOwners
         if (viewTreeOwners != null) {
             callback(viewTreeOwners)
@@ -514,6 +549,18 @@
     }
 
     /**
+     * Android has an issue where calling showSoftwareKeyboard after calling
+     * hideSoftwareKeyboard, it results in keyboard flickering and sometimes the keyboard ends up
+     * being hidden even though the most recent call was to showKeyboard.
+     *
+     * This function starts a suspended function that listens for show/hide commands and only
+     * runs the latest command.
+     */
+    suspend fun keyboardVisibilityEventLoop() {
+        textInputServiceAndroid.keyboardVisibilityEventLoop()
+    }
+
+    /**
      * Walks the entire LayoutNode sub-hierarchy and marks all nodes as needing measurement.
      */
     private fun invalidateLayoutNodeMeasurement(node: LayoutNode) {
@@ -554,7 +601,7 @@
                     "Composed into the View which doesn't propagate" +
                         "ViewTreeSavedStateRegistryOwner!"
                 )
-            val viewTreeOwners = AndroidOwner.ViewTreeOwners(
+            val viewTreeOwners = ViewTreeOwners(
                 lifecycleOwner = lifecycleOwner,
                 viewModelStoreOwner = viewModelStoreOwner,
                 savedStateRegistryOwner = savedStateRegistryOwner
@@ -576,14 +623,6 @@
         }
         viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
         viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
-
-        // In case of benchmarks, the handler callbacks will never get executed as benchmarks block
-        // the main thread. However this callback holds references that point to this view which
-        // effectively prevents it from being garbage collected in benchmarks.
-        if (observationClearRequested) {
-            observationClearRequested = false
-            handler.removeCallbacks(clearInvalidObservations)
-        }
     }
 
     override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
@@ -646,6 +685,10 @@
         return accessibilityDelegate.dispatchHoverEvent(event)
     }
 
+    override val isLifecycleInResumedState: Boolean
+        get() = viewTreeOwners?.lifecycleOwner
+            ?.lifecycle?.currentState == Lifecycle.State.RESUMED
+
     companion object {
         private var systemPropertiesClass: Class<*>? = null
         private var getBooleanMethod: Method? = null
@@ -666,6 +709,24 @@
             false
         }
     }
+
+    /**
+     * Combines objects populated via ViewTree*Owner
+     */
+    class ViewTreeOwners(
+        /**
+         * The [LifecycleOwner] associated with this owner.
+         */
+        val lifecycleOwner: LifecycleOwner,
+        /**
+         * The [ViewModelStoreOwner] associated with this owner.
+         */
+        val viewModelStoreOwner: ViewModelStoreOwner,
+        /**
+         * The [SavedStateRegistryOwner] associated with this owner.
+         */
+        val savedStateRegistryOwner: SavedStateRegistryOwner
+    )
 }
 
 /**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
index 4dba953cc..dd24db3c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
@@ -236,9 +236,9 @@
             ParcelSafeTextLength
         )
         info.stateDescription =
-            semanticsNode.config.getOrNull(SemanticsProperties.AccessibilityValue)
+            semanticsNode.config.getOrNull(SemanticsProperties.StateDescription)
         info.contentDescription =
-            semanticsNode.config.getOrNull(SemanticsProperties.AccessibilityLabel)
+            semanticsNode.config.getOrNull(SemanticsProperties.ContentDescription)
         // Note editable is not added to semantics properties api.
         info.isEditable = semanticsNode.config.contains(SemanticsActions.SetText)
         info.isEnabled = (semanticsNode.config.getOrNull(SemanticsProperties.Disabled) == null)
@@ -334,7 +334,7 @@
                 AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER or
                 AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD or
                 AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PARAGRAPH
-            // We only traverse the text when accessibilityLabel is not set.
+            // We only traverse the text when contentDescription is not set.
             if (info.contentDescription.isNullOrEmpty() &&
                 semanticsNode.config.contains(SemanticsActions.GetTextLayoutResult)
             ) {
@@ -1186,13 +1186,13 @@
                     continue
                 }
                 when (entry.key) {
-                    SemanticsProperties.AccessibilityValue ->
+                    SemanticsProperties.StateDescription ->
                         sendEventForVirtualView(
                             semanticsNodeIdToAccessibilityVirtualNodeId(id),
                             AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
                             AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION
                         )
-                    SemanticsProperties.AccessibilityLabel ->
+                    SemanticsProperties.ContentDescription ->
                         sendEventForVirtualView(
                             semanticsNodeIdToAccessibilityVirtualNodeId(id),
                             AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
@@ -1489,8 +1489,8 @@
     }
 
     private fun getAccessibilitySelectionStart(node: SemanticsNode): Int {
-        // If there is AccessibilityLabel, it will be used instead of text during traversal.
-        if (!node.config.contains(SemanticsProperties.AccessibilityLabel) &&
+        // If there is ContentDescription, it will be used instead of text during traversal.
+        if (!node.config.contains(SemanticsProperties.ContentDescription) &&
             node.config.contains(SemanticsProperties.TextSelectionRange)
         ) {
             return node.config[SemanticsProperties.TextSelectionRange].start
@@ -1499,8 +1499,8 @@
     }
 
     private fun getAccessibilitySelectionEnd(node: SemanticsNode): Int {
-        // If there is AccessibilityLabel, it will be used instead of text during traversal.
-        if (!node.config.contains(SemanticsProperties.AccessibilityLabel) &&
+        // If there is ContentDescription, it will be used instead of text during traversal.
+        if (!node.config.contains(SemanticsProperties.ContentDescription) &&
             node.config.contains(SemanticsProperties.TextSelectionRange)
         ) {
             return node.config[SemanticsProperties.TextSelectionRange].end
@@ -1510,7 +1510,7 @@
 
     private fun isAccessibilitySelectionExtendable(node: SemanticsNode): Boolean {
         // Currently only TextField is extendable. Static text may become extendable later.
-        return !node.config.contains(SemanticsProperties.AccessibilityLabel) &&
+        return !node.config.contains(SemanticsProperties.ContentDescription) &&
             node.config.contains(SemanticsProperties.Text)
     }
 
@@ -1584,8 +1584,8 @@
         }
         // Note in android framework, TextView set this to its text. This is changed to
         // prioritize content description, even for Text.
-        if (node.config.contains(SemanticsProperties.AccessibilityLabel)) {
-            return node.config[SemanticsProperties.AccessibilityLabel]
+        if (node.config.contains(SemanticsProperties.ContentDescription)) {
+            return node.config[SemanticsProperties.ContentDescription]
         }
         if (node.config.contains(SemanticsProperties.Text)) {
             return node.config[SemanticsProperties.Text].text
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidOwner.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidOwner.kt
deleted file mode 100644
index 2cc6d44..0000000
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidOwner.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.compose.ui.platform
-
-import android.content.res.Configuration
-import android.graphics.Canvas
-import android.view.View
-import androidx.annotation.RestrictTo
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.viewinterop.AndroidViewHolder
-import androidx.compose.ui.viewinterop.InternalInteropApi
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ViewModelStoreOwner
-import androidx.savedstate.SavedStateRegistryOwner
-import org.jetbrains.annotations.TestOnly
-
-/**
- * Interface to be implemented by [Owner]s able to handle Android View specific functionality.
- */
-interface AndroidOwner : Owner {
-
-    /**
-     * The view backing this Owner.
-     */
-    val view: View
-
-    /**
-     * Called to inform the owner that a new Android [View] was [attached][Owner.onAttach]
-     * to the hierarchy.
-     */
-    @OptIn(InternalInteropApi::class)
-    @ExperimentalLayoutNodeApi
-    fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode)
-
-    /**
-     * Called to inform the owner that an Android [View] was [detached][Owner.onDetach]
-     * from the hierarchy.
-     */
-    @OptIn(InternalInteropApi::class)
-    fun removeAndroidView(view: AndroidViewHolder)
-
-    /**
-     * Called to ask the owner to draw a child Android [View] to [canvas].
-     */
-    @OptIn(InternalInteropApi::class)
-    fun drawAndroidView(view: AndroidViewHolder, canvas: Canvas)
-
-    /**
-     * Used for updating the ConfigurationAmbient when configuration changes - consume the
-     * configuration ambient instead of changing this observer if you are writing a component
-     * that adapts to configuration changes.
-     */
-    var configurationChangeObserver: (Configuration) -> Unit
-
-    /**
-     * Current [ViewTreeOwners]. Use [setOnViewTreeOwnersAvailable] if you want to
-     * execute your code when the object will be created.
-     */
-    val viewTreeOwners: ViewTreeOwners?
-
-    /**
-     * The callback to be executed when [viewTreeOwners] is created and not-null anymore.
-     * Note that this callback will be fired inline when it is already available
-     */
-    fun setOnViewTreeOwnersAvailable(callback: (ViewTreeOwners) -> Unit)
-
-    /**
-     * Called to invalidate the Android [View] sub-hierarchy handled by this Owner.
-     */
-    fun invalidateDescendants()
-
-    /** @suppress */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    companion object {
-        /**
-         * Called after an [AndroidOwner] is created. Used by AndroidComposeTestRule to keep
-         * track of all attached [AndroidComposeView]s. Not to be set or used by any other
-         * component.
-         */
-        var onAndroidOwnerCreatedCallback: ((AndroidOwner) -> Unit)? = null
-            @TestOnly
-            set
-    }
-
-    /**
-     * Combines objects populated via ViewTree*Owner
-     */
-    class ViewTreeOwners(
-        /**
-         * The [LifecycleOwner] associated with this owner.
-         */
-        val lifecycleOwner: LifecycleOwner,
-        /**
-         * The [ViewModelStoreOwner] associated with this owner.
-         */
-        val viewModelStoreOwner: ViewModelStoreOwner,
-        /**
-         * The [SavedStateRegistryOwner] associated with this owner.
-         */
-        val savedStateRegistryOwner: SavedStateRegistryOwner
-    )
-}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.kt
index aab323a..34b1cd2 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.kt
@@ -13,8 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@file:OptIn(ExperimentalLayoutNodeApi::class)
-
 package androidx.compose.ui.platform
 
 import android.annotation.SuppressLint
@@ -24,7 +22,6 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewParent
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.LayoutState.NeedsRemeasure
 import androidx.compose.ui.viewinterop.AndroidViewHolder
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt
new file mode 100644
index 0000000..b5c4e4e
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform
+
+import android.view.View
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.util.annotation.VisibleForTesting
+
+/**
+ * The marker interface to be implemented by the [View] backing the composition.
+ * To be used in tests.
+ */
+@VisibleForTesting
+// TODO(b/174747742) Introduce RootForTest and extend it instead of Owner
+interface ViewRootForTest : Owner {
+
+    /**
+     * The view backing this Owner.
+     */
+    val view: View
+
+    /**
+     * Returns true when the associated LifecycleOwner is in the resumed state
+     */
+    val isLifecycleInResumedState: Boolean
+
+    /**
+     * Whether the Owner has pending layout work.
+     */
+    val hasPendingMeasureOrLayout: Boolean
+
+    /**
+     * Called to invalidate the Android [View] sub-hierarchy handled by this [View].
+     */
+    fun invalidateDescendants()
+
+    companion object {
+        /**
+         * Called after an View implementing [ViewRootForTest] is created. Used by
+         * AndroidComposeTestRule to keep track of all attached ComposeViews. Not to be
+         * set or used by any other component.
+         */
+        @VisibleForTesting
+        var onViewCreatedCallback: ((ViewRootForTest) -> Unit)? = null
+    }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
index 562a02c..2728ac1 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
@@ -24,18 +24,18 @@
 import androidx.annotation.MainThread
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionData
 import androidx.compose.runtime.CompositionReference
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.Recomposer
-import androidx.compose.runtime.SlotTable
 import androidx.compose.runtime.compositionFor
 import androidx.compose.runtime.currentComposer
 import androidx.compose.runtime.emptyContent
 import androidx.compose.runtime.tooling.InspectionTables
 import androidx.compose.ui.R
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.UiApplier
 import androidx.lifecycle.Lifecycle
@@ -111,8 +111,8 @@
 // nextFrame() inside recompose() doesn't really start a new frame, but a new subframe
 // instead.
 @MainThread
-@OptIn(ExperimentalComposeApi::class, ExperimentalLayoutNodeApi::class)
-internal actual fun actualSubcomposeInto(
+@OptIn(ExperimentalComposeApi::class)
+internal actual fun subcomposeInto(
     container: LayoutNode,
     parent: CompositionReference,
     composable: @Composable () -> Unit
@@ -140,9 +140,9 @@
     content: @Composable () -> Unit
 ): Composition {
     GlobalSnapshotManager.ensureStarted()
-    val composeView: AndroidOwner = window.decorView
+    val composeView: AndroidComposeView = window.decorView
         .findViewById<ViewGroup>(android.R.id.content)
-        .getChildAt(0) as? AndroidOwner
+        .getChildAt(0) as? AndroidComposeView
         ?: AndroidComposeView(this).also {
             setContentView(it.view, DefaultLayoutParams)
         }
@@ -169,7 +169,7 @@
     GlobalSnapshotManager.ensureStarted()
     val composeView =
         if (childCount > 0) {
-            getChildAt(0) as? AndroidOwner
+            getChildAt(0) as? AndroidComposeView
         } else {
             removeAllViews(); null
         } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
@@ -178,14 +178,14 @@
 
 @OptIn(InternalComposeApi::class)
 private fun doSetContent(
-    owner: AndroidOwner,
+    owner: AndroidComposeView,
     parent: CompositionReference,
     content: @Composable () -> Unit
 ): Composition {
     if (inspectionWanted(owner)) {
-        owner.view.setTag(
+        owner.setTag(
             R.id.inspection_slot_table_set,
-            Collections.newSetFromMap(WeakHashMap<SlotTable, Boolean>())
+            Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
         )
         enableDebugInspectorInfo()
     }
@@ -213,36 +213,44 @@
 }
 
 private class WrappedComposition(
-    val owner: AndroidOwner,
+    val owner: AndroidComposeView,
     val original: Composition
 ) : Composition, LifecycleEventObserver {
 
     private var disposed = false
     private var addedToLifecycle: Lifecycle? = null
-    private var contentWaitingForCreated: @Composable () -> Unit = emptyContent()
+    private var lastContent: @Composable () -> Unit = emptyContent()
 
     @OptIn(InternalComposeApi::class)
     override fun setContent(content: @Composable () -> Unit) {
         owner.setOnViewTreeOwnersAvailable {
             if (!disposed) {
                 val lifecycle = it.lifecycleOwner.lifecycle
+                lastContent = content
                 if (addedToLifecycle == null) {
-                    lifecycle.addObserver(this)
                     addedToLifecycle = lifecycle
-                }
-                if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
+                    // this will call ON_CREATE synchronously if we already created
+                    lifecycle.addObserver(this)
+                } else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
                     original.setContent {
                         @Suppress("UNCHECKED_CAST")
                         val inspectionTable =
-                            owner.view.getTag(R.id.inspection_slot_table_set) as?
-                                MutableSet<SlotTable>
-                        inspectionTable?.add(currentComposer.slotTable)
+                            owner.getTag(R.id.inspection_slot_table_set) as?
+                                MutableSet<CompositionData>
+                                ?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
+                                    as? MutableSet<CompositionData>
+                        if (inspectionTable != null) {
+                            @OptIn(InternalComposeApi::class)
+                            inspectionTable.add(currentComposer.compositionData)
+                            currentComposer.collectParameterInformation()
+                        }
+
+                        LaunchedEffect(owner) { owner.keyboardVisibilityEventLoop() }
+
                         Providers(InspectionTables provides inspectionTable) {
                             ProvideAndroidAmbients(owner, content)
                         }
                     }
-                } else {
-                    contentWaitingForCreated = content
                 }
             }
         }
@@ -264,8 +272,7 @@
             dispose()
         } else if (event == Lifecycle.Event.ON_CREATE) {
             if (!disposed) {
-                setContent(contentWaitingForCreated)
-                contentWaitingForCreated = emptyContent()
+                setContent(lastContent)
             }
         }
     }
@@ -286,6 +293,6 @@
  *
  * Instead check if the attributeSourceResourceMap is not empty.
  */
-private fun inspectionWanted(owner: AndroidOwner): Boolean =
+private fun inspectionWanted(owner: AndroidComposeView): Boolean =
     Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
-        owner.view.attributeSourceResourceMap.isNotEmpty()
+        owner.attributeSourceResourceMap.isNotEmpty()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
index c377937..b8ad9f3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
@@ -30,6 +30,8 @@
  *
  * Note: This API is transient and will be likely removed for encouraging async resource loading.
  *
+ * For loading generic loading of rasterized or vector assets see [painterResource]
+ *
  * @param id the resource identifier
  * @return the decoded image data associated with the resource
  */
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt
new file mode 100644
index 0000000..d8af23f
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.res
+
+import android.content.res.Resources
+import android.util.TypedValue
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.imageFromResource
+import androidx.compose.ui.graphics.painter.ImagePainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.graphics.vector.VectorPainter
+import androidx.compose.ui.graphics.vector.compat.seekToStartTag
+import androidx.compose.ui.platform.AmbientContext
+
+/**
+ * Create a [Painter] from an Android resource id. This can load either an instance of
+ * [ImagePainter] or [VectorPainter] for [ImageBitmap] based assets or vector based assets
+ * respectively. The resources with the given id must point to either fully rasterized
+ * images (ex. PNG or JPG files) or VectorDrawable xml assets. API based xml Drawables
+ * are not supported here.
+ *
+ * Example:
+ * @sample androidx.compose.ui.samples.PainterResourceSample
+ *
+ * Alternative Drawable implementations can be used with compose by calling
+ * [drawIntoCanvas] and drawing with the Android framework canvas provided through [nativeCanvas]
+ *
+ * Example:
+ * @sample androidx.compose.ui.samples.AndroidDrawableInDrawScopeSample
+ *
+ * @param id Resources object to query the image file from
+ *
+ * @return [Painter] used for drawing the loaded resource
+ */
+@Composable
+fun painterResource(@DrawableRes id: Int): Painter {
+    val context = AmbientContext.current
+    val res = context.resources
+    val value = remember { TypedValue() }
+    res.getValue(id, value, true)
+    val path = value.string
+    // Assume .xml suffix implies loading a VectorDrawable resource
+    return if (path?.endsWith(".xml") == true) {
+        val imageVector = remember(path, id) {
+            loadVectorResource(context.theme, res, id)
+        }
+        rememberVectorPainter(imageVector)
+    } else {
+        // Otherwise load the bitmap resource
+        val imageBitmap = remember(path, id) {
+            loadImageBitmapResource(res, id)
+        }
+        ImagePainter(imageBitmap)
+    }
+}
+
+/**
+ * Helper method to validate that the xml resource is a vector drawable then load
+ * the ImageVector. Because this throws exceptions we cannot have this implementation as part of
+ * the composable implementation it is invoked in.
+ */
+private fun loadVectorResource(theme: Resources.Theme, res: Resources, id: Int): ImageVector {
+    @Suppress("ResourceType") val parser = res.getXml(id)
+    if (parser.seekToStartTag().name != "vector") {
+        throw IllegalArgumentException(errorMessage)
+    }
+    return loadVectorResourceInner(theme, res, parser)
+}
+
+/**
+ * Helper method to validate the asset resource is a supported resource type and returns
+ * an ImageBitmap resource. Because this throws exceptions we cannot have this implementation
+ * as part of the composable implementation it is invoked in.
+ */
+private fun loadImageBitmapResource(res: Resources, id: Int): ImageBitmap {
+    try {
+        return imageFromResource(res, id)
+    } catch (throwable: Throwable) {
+        throw IllegalArgumentException(errorMessage)
+    }
+}
+
+private const val errorMessage =
+    "Only VectorDrawables and rasterized asset types are supported ex. PNG, JPG"
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
index 8f59986..3fd252b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
@@ -16,8 +16,8 @@
 
 package androidx.compose.ui.res
 
-import android.annotation.SuppressLint
 import android.content.res.Resources
+import android.content.res.XmlResourceParser
 import android.util.TypedValue
 import android.util.Xml
 import androidx.annotation.DrawableRes
@@ -39,6 +39,8 @@
  * based off of it's dimensions appropriately
  *
  * Note: This API is transient and will be likely removed for encouraging async resource loading.
+ *
+ * For loading generic loading of rasterized or vector assets see [painterResource]
  */
 @Composable
 fun vectorResource(@DrawableRes id: Int): ImageVector {
@@ -57,6 +59,8 @@
  * [PendingResource]. Once the loading finishes, recompose is scheduled and this function will
  * return deferred vector drawable resource with [LoadedResource] or [FailedResource].
  *
+ * For loading generic loading of rasterized or vector assets see [painterResource]
+ *
  * @param id the resource identifier
  * @param pendingResource an optional resource to be used during loading instead.
  * @param failedResource an optional resource to be used if resource loading failed.
@@ -90,11 +94,23 @@
 internal fun loadVectorResource(
     theme: Resources.Theme? = null,
     res: Resources,
-    resId: Int
+    resId: Int,
+): ImageVector =
+    loadVectorResourceInner(theme, res, res.getXml(resId).apply { seekToStartTag() })
+
+/**
+ * Helper method that parses a vector asset from the given [XmlResourceParser] position.
+ * This method assumes the parser is already been positioned to the start tag
+ */
+@Throws(XmlPullParserException::class)
+@SuppressWarnings("RestrictedApi")
+internal fun loadVectorResourceInner(
+    theme: Resources.Theme? = null,
+    res: Resources,
+    parser: XmlResourceParser
 ): ImageVector {
-    @SuppressLint("ResourceType") val parser = res.getXml(resId)
     val attrs = Xml.asAttributeSet(parser)
-    val builder = parser.seekToStartTag().createVectorImageBuilder(res, theme, attrs)
+    val builder = parser.createVectorImageBuilder(res, theme, attrs)
 
     var nestedGroups = 0
     while (!parser.isAtEnd()) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
index feaece2..264c0fc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
@@ -29,6 +29,8 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextRange
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.channels.Channel
 import kotlin.math.roundToInt
 
 /**
@@ -56,6 +58,12 @@
      */
     private lateinit var imm: InputMethodManager
 
+    /**
+     * A channel that is used to send ShowKeyboard/HideKeyboard commands. Send 'true' for
+     * show Keyboard and 'false' to hide keyboard.
+     */
+    private val showKeyboardChannel = Channel<Boolean>(Channel.CONFLATED)
+
     private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
         // focusedRect is null if there is not ongoing text input session. So safe to request
         // latest focused rectangle whenever global layout has changed.
@@ -138,11 +146,26 @@
     }
 
     override fun showSoftwareKeyboard() {
-        imm.showSoftInput(view, 0)
+        showKeyboardChannel.offer(true)
     }
 
     override fun hideSoftwareKeyboard() {
-        imm.hideSoftInputFromWindow(view.windowToken, 0)
+        showKeyboardChannel.offer(false)
+    }
+
+    @OptIn(FlowPreview::class)
+    suspend fun keyboardVisibilityEventLoop() {
+        for (showKeyboard in showKeyboardChannel) {
+            // Even though we are using a conflated channel, and the producers and consumers are
+            // on the same thread, there is a possibility that we have a stale value in the channel
+            // because we start consuming from it before we finish producing all the values. We poll
+            // to make sure that we use the most recent value.
+            if (showKeyboardChannel.poll() ?: showKeyboard) {
+                imm.showSoftInput(view, 0)
+            } else {
+                imm.hideSoftInputFromWindow(view.windowToken, 0)
+            }
+        }
     }
 
     override fun onStateUpdated(oldValue: TextFieldValue?, newValue: TextFieldValue) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt
index a5d6337..d9fcb3a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt
@@ -31,6 +31,7 @@
 import androidx.compose.runtime.Composition
 import androidx.compose.runtime.CompositionReference
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.compositionReference
 import androidx.compose.runtime.onActive
 import androidx.compose.runtime.onCommit
@@ -55,11 +56,17 @@
 /**
  * Android specific properties to configure a dialog.
  *
- * @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the dialog's
- * window.
+ * @property dismissOnClickOutside whether the dialog can be dismissed by pressing the back button.
+ * If true, pressing the back button will call onDismissRequest.
+ * @property dismissOnClickOutside whether the dialog can be dismissed by clicking outside the
+ * dialog's bounds. If true, clicking outside the dialog will call onDismissRequest.
+ * @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
+ * dialog's window.
  */
 @Immutable
 data class AndroidDialogProperties(
+    val dismissOnBackPress: Boolean = true,
+    val dismissOnClickOutside: Boolean = true,
     val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit
 ) : DialogProperties
 
@@ -88,8 +95,8 @@
     val density = AmbientDensity.current
 
     val dialog = remember(view, density) { DialogWrapper(view, density) }
-    dialog.onCloseRequest = onDismissRequest
-    remember(properties) { dialog.setProperties(properties) }
+    dialog.onDismissRequest = onDismissRequest
+    SideEffect { dialog.setProperties(properties) }
 
     onActive {
         dialog.show()
@@ -138,10 +145,11 @@
      */
     ContextThemeWrapper(composeView.context, R.style.DialogWindowTheme)
 ) {
-    lateinit var onCloseRequest: () -> Unit
+    lateinit var onDismissRequest: () -> Unit
 
     private val dialogLayout: DialogLayout
     private var composition: Composition? = null
+    private var properties: AndroidDialogProperties = AndroidDialogProperties()
 
     private val maxSupportedElevation = 30.dp
 
@@ -209,15 +217,13 @@
         )
     }
 
-    fun setProperties(properties: DialogProperties?) {
-        if (properties != null && properties is AndroidDialogProperties) {
-            setSecureFlagEnabled(
-                properties.securePolicy
-                    .shouldApplySecureFlag(composeView.isFlagSecureEnabled())
-            )
-        } else {
-            setSecureFlagEnabled(composeView.isFlagSecureEnabled())
+    fun setProperties(newProperties: DialogProperties?) {
+        if (newProperties is AndroidDialogProperties) {
+            properties = newProperties
         }
+        setSecureFlagEnabled(
+            properties.securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
+        )
     }
 
     fun disposeComposition() {
@@ -226,8 +232,8 @@
 
     override fun onTouchEvent(event: MotionEvent): Boolean {
         val result = super.onTouchEvent(event)
-        if (result) {
-            onCloseRequest()
+        if (result && properties.dismissOnClickOutside) {
+            onDismissRequest()
         }
 
         return result
@@ -239,7 +245,9 @@
     }
 
     override fun onBackPressed() {
-        onCloseRequest()
+        if (properties.dismissOnBackPress) {
+            onDismissRequest()
+        }
     }
 }
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
index 617ab73..01e2021 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
@@ -30,6 +30,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composition
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.compositionReference
 import androidx.compose.runtime.emptyContent
 import androidx.compose.runtime.onCommit
@@ -95,9 +96,11 @@
     // Refresh anything that might have changed
     popupLayout.onDismissRequest = onDismissRequest
     popupLayout.testTag = AmbientPopupTestTag.current
-    remember(popupPositionProvider) { popupLayout.setPositionProvider(popupPositionProvider) }
-    remember(isFocusable) { popupLayout.setIsFocusable(isFocusable) }
-    remember(properties) { popupLayout.setProperties(properties) }
+    SideEffect {
+        popupLayout.setPositionProvider(popupPositionProvider)
+        popupLayout.setIsFocusable(isFocusable)
+        popupLayout.setProperties(properties)
+    }
 
     var composition: Composition? = null
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
index 30a3c4d..715a43f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
@@ -13,20 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.ui
 
-import androidx.compose.ui.graphics.drawscope.DrawScope
+package androidx.compose.ui
 
 /**
  * Receiver scope for drawing content into a layout, where the content can
  * be drawn between other canvas operations. If [drawContent] is not called,
  * the contents of the layout will not be drawn.
- *
- * @see DrawModifier
  */
-interface ContentDrawScope : DrawScope {
-    /**
-     * Causes child drawing operations to run during the `onPaint` lambda.
-     */
-    fun drawContent()
-}
+@Deprecated(
+    "Use ContentDrawScope in the graphics package instead",
+    ReplaceWith(
+        "ContentDrawScope",
+        "androidx.compose.ui.graphics.drawscope.ContentDrawScope"
+    )
+)
+typealias ContentDrawScope = androidx.compose.ui.graphics.drawscope.ContentDrawScope
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
index 2c5e783..8a2591f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
@@ -117,6 +117,7 @@
 )
 typealias DrawResult = androidx.compose.ui.draw.DrawResult
 
+@Suppress("DEPRECATION")
 @Deprecated(
     "Use drawWithContent from the androidx.compose.ui.draw package instead",
     ReplaceWith(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
similarity index 79%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
index f9cb2fe..84259b4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+package androidx.compose.ui
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+@RequiresOptIn("This API is experimental and is likely to change in the future.")
+annotation class ExperimentalComposeUiApi
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt
index a8ac09c..20e341a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt
@@ -17,7 +17,6 @@
 package androidx.compose.ui
 
 import androidx.compose.runtime.remember
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.FocusState.Inactive
 import androidx.compose.ui.node.ModifiedFocusNode
@@ -30,7 +29,6 @@
  * A [Modifier.Element] that wraps makes the modifiers on the right into a Focusable. Use a
  * different instance of [FocusModifier] for each focusable component.
  */
-@OptIn(ExperimentalFocus::class)
 internal class FocusModifier(
     initialFocus: FocusState,
     // TODO(b/172265016): Make this a required parameter and remove the default value.
@@ -42,7 +40,7 @@
     var focusState: FocusState = initialFocus
         set(value) {
             field = value
-            focusNode.wrappedBy?.propagateFocusStateChange(value)
+            focusNode.wrappedBy?.propagateFocusEvent(value)
         }
 
     var focusedChild: ModifiedFocusNode? = null
@@ -53,7 +51,6 @@
 /**
  * Add this modifier to a component to make it focusable.
  */
-@ExperimentalFocus
 fun Modifier.focus(): Modifier = composed(inspectorInfo = debugInspectorInfo { name = "focus" }) {
     remember { FocusModifier(Inactive) }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt
index 4546434..8719828 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt
@@ -16,16 +16,19 @@
 
 package androidx.compose.ui
 
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusState
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
 
+// TODO(b/174728671): Remove this deprecated API after Alpha 09
 /**
  * A [modifier][Modifier.Element] that can be used to observe focus state changes.
  */
-@ExperimentalFocus
+@Deprecated(
+    "Please use FocusEventModifier",
+    replaceWith = ReplaceWith(
+        "FocusEventModifier",
+        "androidx.compose.ui.focus.FocusEventModifier"
+    )
+)
 interface FocusObserverModifier : Modifier.Element {
     /**
      * A callback that is called whenever focus state changes.
@@ -33,24 +36,19 @@
     val onFocusChange: (FocusState) -> Unit
 }
 
-@OptIn(ExperimentalFocus::class)
-internal class FocusObserverModifierImpl(
-    override val onFocusChange: (FocusState) -> Unit,
-    inspectorInfo: InspectorInfo.() -> Unit
-) : FocusObserverModifier, InspectorValueInfo(inspectorInfo)
-
+// TODO(b/174728671): Remove this deprecated API after Alpha 09
 /**
  * Add this modifier to a component to observe focus state changes.
  */
-@ExperimentalFocus
-fun Modifier.focusObserver(onFocusChange: (FocusState) -> Unit): Modifier {
-    return this.then(
-        FocusObserverModifierImpl(
-            onFocusChange = onFocusChange,
-            inspectorInfo = debugInspectorInfo {
-                name = "focusObserver"
-                properties["onFocusChange"] = onFocusChange
-            }
-        )
+@Suppress("unused", "UNUSED_PARAMETER")
+@Deprecated(
+    message = "Please use either onFocusChanged or onFocusEvent",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith(
+        "onFocusChanged(onFocusChange)",
+        "androidx.compose.ui.focus.onFocusChanged"
     )
-}
\ No newline at end of file
+)
+fun Modifier.focusObserver(onFocusChange: (FocusState) -> Unit): Modifier {
+    return Modifier
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt
index bd049b0..5d9a17d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui
 
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
@@ -28,7 +27,6 @@
  *
  * @see FocusRequester
  */
-@ExperimentalFocus
 interface FocusRequesterModifier : Modifier.Element {
     /**
      * An instance of [FocusRequester], that can be used to request focus state changes.
@@ -36,7 +34,6 @@
     val focusRequester: FocusRequester
 }
 
-@OptIn(ExperimentalFocus::class)
 internal class FocusRequesterModifierImpl(
     override val focusRequester: FocusRequester,
     inspectorInfo: InspectorInfo.() -> Unit
@@ -45,7 +42,6 @@
 /**
  * Add this modifier to a component to observe changes to focus state.
  */
-@ExperimentalFocus
 fun Modifier.focusRequester(focusRequester: FocusRequester): Modifier {
     return this.then(
         FocusRequesterModifierImpl(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
index 09f55f3..2fca515 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.autofill
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.util.annotation.GuardedBy
 
@@ -26,6 +27,7 @@
  * or cancel autofill as required. For instance, the [TextField] can call [requestAutofillForNode]
  * when it gains focus, and [cancelAutofillForNode] when it loses focus.
  */
+@ExperimentalComposeUiApi
 interface Autofill {
 
     /**
@@ -66,6 +68,7 @@
  *
  * @property id A virtual id that is automatically generated for each node.
  */
+@ExperimentalComposeUiApi
 data class AutofillNode(
     val autofillTypes: List<AutofillType> = listOf(),
     var boundingBox: Rect? = null,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt
index da01afb..5a8eb84 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.ui.autofill
 
+import androidx.compose.ui.ExperimentalComposeUiApi
+
 /**
  * The autofill tree is a temporary data structure that is used before the Semantics Tree is
  * implemented. This data structure is used by compose components to set autofill
@@ -27,6 +29,7 @@
  * Since this is a temporary implementation, it is implemented as a list of [children], which is
  * essentially a tree of height = 1
  */
+@ExperimentalComposeUiApi
 class AutofillTree {
     /**
      * A map which contains [AutofillNode]s, where every node represents an autofillable field.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
index 54af0f4..f0e1b16 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.ui.autofill
 
+import androidx.compose.ui.ExperimentalComposeUiApi
+
 /**
  * Autofill type information.
  *
@@ -24,6 +26,7 @@
  * to use heuristics to determine the right value to use while
  * autofilling the corresponding field.
  */
+@ExperimentalComposeUiApi
 enum class AutofillType {
     /**
      * Indicates that the associated component can be aufofilled with an email address.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
index a2f6821..43ba66b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
@@ -17,11 +17,11 @@
 package androidx.compose.ui.draw
 
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index 378184a..f50f987 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -17,7 +17,7 @@
 package androidx.compose.ui.draw
 
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Size
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
new file mode 100644
index 0000000..cc14f53
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.platform.debugInspectorInfo
+
+/**
+ * Add this modifier to a component to observe focus state events. [onFocusChanged] is invoked
+ * only when the focus state changes.
+ *
+ * If you want to be notified every time the internal focus state is written to (even if it
+ * hasn't changed), use [onFocusEvent] instead.
+ */
+fun Modifier.onFocusChanged(onFocusChanged: (FocusState) -> Unit): Modifier =
+    composed(
+        inspectorInfo = debugInspectorInfo {
+            name = "onFocusChanged"
+            properties["onFocusChanged"] = onFocusChanged
+        }
+    ) {
+        val focusState: MutableState<FocusState?> = remember { mutableStateOf(null) }
+        Modifier.onFocusEvent {
+            if (focusState.value != it) {
+                focusState.value = it
+                onFocusChanged(it)
+            }
+        }
+    }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
new file mode 100644
index 0000000..87a3749
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+
+/**
+ * A [modifier][Modifier.Element] that can be used to observe focus state events.
+ */
+interface FocusEventModifier : Modifier.Element {
+    /**
+     * A callback that is called whenever the focus system raises events.
+     */
+    fun onFocusEvent(focusState: FocusState)
+}
+
+internal class FocusEventModifierImpl(
+    val onFocusEvent: (FocusState) -> Unit,
+    inspectorInfo: InspectorInfo.() -> Unit
+) : FocusEventModifier, InspectorValueInfo(inspectorInfo) {
+    override fun onFocusEvent(focusState: FocusState) {
+        onFocusEvent.invoke(focusState)
+    }
+}
+
+/**
+ * Add this modifier to a component to observe focus state events.
+ */
+fun Modifier.onFocusEvent(onFocusEvent: (FocusState) -> Unit): Modifier {
+    return this.then(
+        FocusEventModifierImpl(
+            onFocusEvent = onFocusEvent,
+            inspectorInfo = debugInspectorInfo {
+                name = "onFocusEvent"
+                properties["onFocusEvent"] = onFocusEvent
+            }
+        )
+    )
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
index f396c48..8e93efa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
@@ -23,7 +23,6 @@
 import androidx.compose.ui.gesture.PointerInputModifierImpl
 import androidx.compose.ui.gesture.TapGestureFilter
 
-@ExperimentalFocus
 interface FocusManager {
     /**
      * Call this function to clear focus from the currently focused component, and set the focus to
@@ -41,7 +40,6 @@
  *
  * @param focusModifier The modifier that will be used as the root focus modifier.
  */
-@ExperimentalFocus
 internal class FocusManagerImpl(
     private val focusModifier: FocusModifier = FocusModifier(Inactive)
 ) : FocusManager {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt
index 30655f4..d0335e3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusNodeUtils.kt
@@ -18,14 +18,12 @@
 
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.ModifiedFocusNode
 import androidx.compose.ui.util.fastForEach
 
 internal val FOCUS_TAG = "Compose Focus"
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal fun LayoutNode.focusableChildren2(): List<ModifiedFocusNode> {
     val focusableChildren = mutableListOf<ModifiedFocusNode>()
     // TODO(b/152529395): Write a test for LayoutNode.focusableChildren(). We were calling the wrong
@@ -43,9 +41,6 @@
  *
  * @param queue a mutable list used as a queue for breadth-first search.
  */
-@OptIn(
-    ExperimentalLayoutNodeApi::class
-)
 internal fun LayoutNode.searchChildrenForFocusNode(
     queue: MutableVector<LayoutNode> = mutableVectorOf()
 ): ModifiedFocusNode? {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index d4a8690..99c0968 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -31,7 +31,6 @@
  *
  * @see androidx.compose.ui.focusRequester
  */
-@ExperimentalFocus
 class FocusRequester {
 
     internal val focusRequesterNodes: MutableVector<ModifiedFocusRequesterNode> = mutableVectorOf()
@@ -98,4 +97,34 @@
         }
         return success
     }
+
+    companion object {
+        /**
+         * Convenient way to create multiple [FocusRequester] instances.
+         */
+        object FocusRequesterFactory {
+            operator fun component1() = FocusRequester()
+            operator fun component2() = FocusRequester()
+            operator fun component3() = FocusRequester()
+            operator fun component4() = FocusRequester()
+            operator fun component5() = FocusRequester()
+            operator fun component6() = FocusRequester()
+            operator fun component7() = FocusRequester()
+            operator fun component8() = FocusRequester()
+            operator fun component9() = FocusRequester()
+            operator fun component10() = FocusRequester()
+            operator fun component11() = FocusRequester()
+            operator fun component12() = FocusRequester()
+            operator fun component13() = FocusRequester()
+            operator fun component14() = FocusRequester()
+            operator fun component15() = FocusRequester()
+            operator fun component16() = FocusRequester()
+        }
+
+        /**
+         * Convenient way to create multiple [FocusRequester]s, which can to be used to request
+         * focus, or to specify a focus traversal order.
+         */
+        fun createRefs() = FocusRequesterFactory
+    }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
index 0182bd3..68a9f7c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
@@ -20,7 +20,6 @@
  * Different states of the focus system. These are the states used by the Focus Nodes.
  *
  */
-@ExperimentalFocus
 enum class FocusState {
     /**
      * The focusable component is currently active (i.e. it receives key events).
@@ -56,7 +55,6 @@
  *
  * @return true if the component is focused, false otherwise.
  */
-@ExperimentalFocus
 val FocusState.isFocused
     get() = when (this) {
         FocusState.Captured,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt
index 39bb158..5d2b6f4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture
 
 import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt
index 5c386f1..141b8ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture
 
 import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt
index 430326b..2fa2b6b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture
 
 import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt
index 57e5807..e3aba9e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture
 
 import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt
index 10ea0f1..b2787cd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt
@@ -17,7 +17,6 @@
 package androidx.compose.ui.gesture.customevents
 
 import androidx.compose.ui.gesture.DoubleTapGestureFilter
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.gesture.TapGestureFilter
 import androidx.compose.ui.input.pointer.CustomEvent
 import androidx.compose.ui.input.pointer.PointerId
@@ -40,7 +39,6 @@
  * @param pointers The pointers whose up events are being requested to be delayed.
  */
 @Suppress("EqualsOrHashCode")
-@ExperimentalPointerInput
 data class DelayUpEvent(var message: DelayUpMessage, val pointers: Set<PointerId>) : CustomEvent {
 
     // Only generating hash code with immutable property.
@@ -52,7 +50,6 @@
 /**
  * The types of messages that can be dispatched.
  */
-@ExperimentalPointerInput
 enum class DelayUpMessage {
     /**
      * Reports that future "up events" should not result in any normally related callbacks at
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollDelegatingWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollDelegatingWrapper.kt
new file mode 100644
index 0000000..6c29a8d
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollDelegatingWrapper.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.gesture.nestedscroll
+
+import androidx.compose.runtime.collection.MutableVector
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.node.DelegatingLayoutNodeWrapper
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.LayoutNodeWrapper
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.minus
+import androidx.compose.ui.unit.plus
+
+internal class NestedScrollDelegatingWrapper(
+    wrapped: LayoutNodeWrapper,
+    nestedScrollModifier: NestedScrollModifier
+) : DelegatingLayoutNodeWrapper<NestedScrollModifier>(wrapped, nestedScrollModifier) {
+
+    // reference to the parent connection to properly dispatch or provide to children when detached
+    private var parentConnection: NestedScrollConnection? = null
+        set(value) {
+            modifier.dispatcher.parent = value
+            childScrollConnection.parent = value ?: NoOpConnection
+            field = value
+        }
+
+    // save last modifier until the next onModifierChanged() call to understand if we got new
+    // connection or a new dispatcher, therefore we need to update self and our children
+    private var lastModifier: NestedScrollModifier? = null
+
+    override fun onModifierChanged() {
+        super.onModifierChanged()
+        childScrollConnection.self = modifier.connection
+        modifier.dispatcher.parent = parentConnection
+        refreshSelfIfNeeded()
+    }
+
+    override var modifier: NestedScrollModifier
+        get() = super.modifier
+        set(value) {
+            lastModifier = super.modifier
+            super.modifier = value
+        }
+
+    override fun attach() {
+        super.attach()
+        refreshSelfIfNeeded()
+    }
+
+    override fun detach() {
+        super.detach()
+        refreshChildrenWithParentConnection(parentConnection)
+        lastModifier = null
+    }
+
+    override fun findPreviousNestedScrollWrapper() = this
+
+    override fun findNextNestedScrollWrapper() = this
+
+    private val childScrollConnection = ParentWrapperNestedScrollConnection(
+        parent = parentConnection ?: NoOpConnection,
+        self = nestedScrollModifier.connection
+    )
+
+    private fun refreshSelfIfNeeded() {
+        val localLastModifier = lastModifier
+        val modifierChanged = localLastModifier == null ||
+            localLastModifier.connection !== modifier.connection ||
+            localLastModifier.dispatcher !== modifier.dispatcher
+        if (modifierChanged && isAttached) {
+            parentConnection = super.findPreviousNestedScrollWrapper()?.childScrollConnection
+            refreshChildrenWithParentConnection(childScrollConnection)
+            lastModifier = modifier
+        }
+    }
+
+    /**
+     * Supply new parent connection for children. Initially children can do it themselves, but
+     * after runtime nestedscroll graph changes parents need to update their children.
+     *
+     * This is O(n) operation, so call only when parent really changes (connection changes,
+     * detach, attach, etc)
+     */
+    private fun refreshChildrenWithParentConnection(newParent: NestedScrollConnection?) {
+        nestedScrollChildrenResult.clear()
+        val nextNestedScrollWrapper = wrapped.findNextNestedScrollWrapper()
+        if (nextNestedScrollWrapper != null) {
+            nestedScrollChildrenResult.add(nextNestedScrollWrapper)
+        } else {
+            loopChildrenForNestedScroll(layoutNode._children)
+        }
+        nestedScrollChildrenResult.forEach {
+            it.parentConnection = newParent
+        }
+    }
+
+    private fun loopChildrenForNestedScroll(children: MutableVector<LayoutNode>) {
+        children.forEach { child ->
+            val nestedScrollChild =
+                child.outerLayoutNodeWrapper.findNextNestedScrollWrapper()
+            if (nestedScrollChild != null) {
+                nestedScrollChildrenResult.add(nestedScrollChild)
+            } else {
+                loopChildrenForNestedScroll(child._children)
+            }
+        }
+    }
+
+    // do not use directly, this is only for optimization.
+    // Populated and returned by findNestedScrollChildren.
+    private val nestedScrollChildrenResult = MutableVector<NestedScrollDelegatingWrapper>()
+}
+
+/**
+ * Parent-child binding contract. This wrapper guarantees pre-scroll/scroll/pre-fling/fling call
+ * order in the nested scroll chain.
+ */
+private class ParentWrapperNestedScrollConnection(
+    var parent: NestedScrollConnection,
+    var self: NestedScrollConnection
+) : NestedScrollConnection {
+
+    override fun onPreScroll(
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset {
+        val parentPreConsumed = parent.onPreScroll(available, source)
+        val selfPreConsumed = self.onPreScroll(available - parentPreConsumed, source)
+        return parentPreConsumed + selfPreConsumed
+    }
+
+    override fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset {
+        val selfConsumed = self.onPostScroll(consumed, available, source)
+        val parentConsumed =
+            parent.onPostScroll(consumed + selfConsumed, available - selfConsumed, source)
+        return selfConsumed + parentConsumed
+    }
+
+    override fun onPreFling(available: Velocity): Velocity {
+        val parentPreConsumed = parent.onPreFling(available)
+        val selfPreConsumed = self.onPreFling(available - parentPreConsumed)
+        return parentPreConsumed + selfPreConsumed
+    }
+
+    override fun onPostFling(
+        consumed: Velocity,
+        available: Velocity,
+        onFinished: (Velocity) -> Unit
+    ) {
+        val selfEnd = { selfConsumed: Velocity ->
+            val parentEnd = { parentConsumed: Velocity ->
+                onFinished.invoke(selfConsumed + parentConsumed)
+            }
+            parent.onPostFling(
+                consumed + selfConsumed,
+                available - selfConsumed,
+                parentEnd
+            )
+        }
+        self.onPostFling(consumed, available, selfEnd)
+    }
+}
+
+/**
+ * No-op parent that consumed nothing. Should be gone by b/174348612
+ */
+private val NoOpConnection: NestedScrollConnection = object : NestedScrollConnection {
+
+    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
+        Offset.Zero
+
+    override fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset =
+        Offset.Zero
+
+    override fun onPreFling(available: Velocity): Velocity = Velocity.Zero
+
+    override fun onPostFling(
+        consumed: Velocity,
+        available: Velocity,
+        onFinished: (Velocity) -> Unit
+    ) {
+        onFinished.invoke(Velocity.Zero)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifier.kt
new file mode 100644
index 0000000..fbbb177
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifier.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.gesture.nestedscroll
+
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Velocity
+
+/**
+ * A [Modifier.Element] that represents nested scroll node in the hierarchy
+ */
+internal interface NestedScrollModifier : Modifier.Element {
+
+    /**
+     * Nested scroll events dispatcher to notify nested scrolling system about scroll events.
+     * This is to be used by the nodes that are scrollable themselves to notify
+     * [NestedScrollConnection]s in the tree.
+     *
+     * Note: The [connection] passed to the [NestedScrollModifier] doesn't count as an ancestor
+     * since it's the node itself
+     */
+    val dispatcher: NestedScrollDispatcher
+
+    /**
+     * Nested scroll connection to participate in the nested scroll events chain. Implementing
+     * this connection allows to react on the nested scroll related events and influence
+     * scrolling descendants and ascendants
+     */
+    val connection: NestedScrollConnection
+}
+
+/**
+ * Interface to connect to the nested scroll system.
+ *
+ * Pass this connection to the [nestedScroll] modifier to participate in the nested scroll
+ * hierarchy and to receive nested scroll events when they are dispatched by the scrolling child
+ * (scrolling child - the element that actually receives scrolling events and dispatches them via
+ * [NestedScrollDispatcher]).
+ *
+ * @see NestedScrollDispatcher to learn how to dispatch nested scroll events to become a
+ * scrolling child
+ * @see nestedScroll to attach this connection to the nested scroll system
+ */
+interface NestedScrollConnection {
+
+    /**
+     * Pre scroll event chain. Called by children to allow parents to consume a portion of a drag
+     * event beforehand
+     *
+     * @param available the delta available to consume for pre scroll
+     * @param source the source of the scroll event
+     *
+     * @see NestedScrollSource
+     *
+     * @return the amount this connection consumed
+     */
+    fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero
+
+    /**
+     * Post scroll event pass. This pass occurs when the dispatching (scrolling) descendant made
+     * their consumption and notifies ancestors with what's left for them to consume.
+     *
+     * @param consumed the amount that was consumed by all nested scroll nodes below the hierarchy
+     * @param available the amount of delta available for this connection to consume
+     * @param source source of the scroll
+     *
+     * @see NestedScrollSource
+     *
+     * @return the amount that was consumed by this connection
+     */
+    fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset = Offset.Zero
+
+    /**
+     * Pre fling event chain. Called by children when they are about to perform fling to
+     * allow parents to intercept and consume part of the initial velocity
+     *
+     * @param available the velocity which is available to pre consume and with which the child
+     * is about to fling
+     *
+     * @return the amount this connection wants to consume and take from the child
+     */
+    fun onPreFling(available: Velocity): Velocity = Velocity.Zero
+
+    /**
+     * Post fling event chain. Called by the child when it is finished flinging (and sending
+     * [onPreScroll] & [onPostScroll] events)
+     *
+     * @param consumed the amount of velocity consumed by the child
+     * @param available the amount of velocity left for a parent to fling after the child (if
+     * desired)
+     * @param onFinished callback to be called when this connection finished flinging, to
+     * be called with the amount of velocity consumed by the fling operation. This callback is
+     * crucial to be called in order to ensure nodes above will receive their [onPostFling].
+     */
+    // TODO: remove notifySelfFinish when b/174485541
+    fun onPostFling(
+        consumed: Velocity,
+        available: Velocity,
+        onFinished: (Velocity) -> Unit
+    ) {
+        onFinished(Velocity.Zero)
+    }
+}
+
+/**
+ * Nested scroll events dispatcher to notify the nested scroll system about the scrolling events
+ * that are happening on the element.
+ *
+ * If the element/modifier itself is able to receive scroll events (from the touch, fling,
+ * mouse, etc) and it would like to respect nested scrolling by notifying elements above, it should
+ * properly dispatch nested scroll events when being scrolled
+ *
+ * It is important to dispatch these events at the right time, provide valid information to the
+ * parents and react to the feedback received from them in order to provide good user experience
+ * with other nested scrolling nodes.
+ *
+ * @see nestedScroll for the reference of the nested scroll process and more details
+ * @see NestedScrollConnection to connect to the nested scroll system
+ */
+class NestedScrollDispatcher {
+
+    /**
+     * Parent to be set when attached to nested scrolling chain. `null` is valid and means there no
+     * nested scrolling parent above
+     */
+    internal var parent: NestedScrollConnection? = null
+
+    /**
+     * Dispatch pre scroll pass. This triggers [NestedScrollConnection.onPreScroll] on all the
+     * ancestors giving them possibility to pre-consume delta if they desire so.
+     *
+     * @param available the delta arrived from a scroll event
+     * @param source the source of the scroll event
+     *
+     * @return total delta that is pre-consumed by all ancestors in the chain. This delta is
+     * unavailable for this node to consume, so it should adjust the consumption accordingly
+     */
+    fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
+        return parent?.onPreScroll(available, source) ?: Offset.Zero
+    }
+
+    /**
+     * Dispatch nested post-scrolling pass. This triggers [NestedScrollConnection.onPostScroll] on
+     * all the ancestors giving them possibility to react of the scroll deltas that are left
+     * after the dispatching node itself and other [NestedScrollConnection]s below consumed the
+     * desired amount.
+     *
+     * @param consumed the amount that this node consumed already
+     * @param available the amount of delta left for ancestors
+     * @param source source of the scroll
+     *
+     * @return the amount of scroll that was consumed by all ancestors
+     */
+    fun dispatchPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset {
+        return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero
+    }
+
+    /**
+     * Dispatch pre fling pass. This triggers [NestedScrollConnection.onPreFling] on all the
+     * ancestors giving them a possibility to react on the fling that is about to happen and
+     * consume part of the velocity.
+     *
+     * @param available velocity from the scroll evens that this node is about to fling with
+     *
+     * @return total velocity that is pre-consumed by all ancestors in the chain. This velocity is
+     * unavailable for this node to consume, so it should adjust the consumption accordingly
+     */
+    fun dispatchPreFling(available: Velocity): Velocity {
+        return parent?.onPreFling(available) ?: Velocity.Zero
+    }
+
+    /**
+     * Dispatch post fling pass. This triggers [NestedScrollConnection.onPostFling] on all the
+     * ancestors, giving them possibility to react of the velocity that is left after the
+     * dispatching node itself flung with the desired amount.
+     *
+     * @param consumed velocity already consumed by this node
+     * @param available velocity that is left for ancestors to consume
+     */
+    fun dispatchPostFling(consumed: Velocity, available: Velocity) {
+        parent?.onPostFling(consumed, available) {}
+    }
+}
+
+/**
+ * Possible sources of scroll events in the [NestedScrollConnection]
+ */
+enum class NestedScrollSource {
+    /**
+     * Dragging via mouse/touch/etc events
+     */
+    Drag,
+
+    /**
+     * Flinging after the drag has ended with velocity
+     */
+    Fling
+}
+
+/**
+ * Modify element to make it participate in the nested scrolling hierarchy.
+ *
+ * There are two ways to participate in the nested scroll: as a scrolling child by dispatching
+ * scrolling events via [NestedScrollDispatcher] to the nested scroll chain; and as a member of
+ * nested scroll chain by providing [NestedScrollConnection], which will be called when another
+ * nested scrolling child below dispatches scrolling events.
+ *
+ * It's a mandatory to participate as a [NestedScrollConnection] in the chain, but scrolling
+ * events dispatch is optional since there are cases when element wants to participate in the
+ * nested scroll, but not a scrollable thing itself.
+ *
+ * Note: It is recommended to reuse [NestedScrollConnection] and [NestedScrollDispatcher] objects
+ * between recompositions since different object will cause nested scroll graph to be
+ * recalculated unnecessary.
+ *
+ * There are 4 main passes in nested scrolling system:
+ *
+ * 1. Pre-scroll. This callback is triggered when the descendant is about to perform a scroll
+ * operation and gives parent an opportunity to consume part of child's delta beforehand. This
+ * pass should happen every time scrollable components receives delta and dispatches it via
+ * [NestedScrollDispatcher]. Dispatching child should take into account how much all ancestors
+ * above the hierarchy consumed and adjust the consumption accordingly.
+ *
+ * 2. Post-scroll. This callback is triggered when the descendant consumed the delta already
+ * (after taking into account what parents pre-consumed in 1.) and wants to notify the ancestors
+ * with the amount of delta unconsumed. This pass should happen every time scrollable components
+ * receives delta and dispatches it via [NestedScrollDispatcher]. Any parent that receives
+ * [NestedScrollConnection.onPostScroll] should consume no more than `left` and return the amount
+ * consumed.
+ *
+ * 3. Pre-fling. Pass that happens when the scrolling descendant stopped dragging and about to
+ * fling with the some velocity. This callback allows ancestors to consume part of the velocity.
+ * This pass should happen before the fling itself happens. Similar to pre-scroll, parent can
+ * consume part of the velocity and nodes below (including the dispatching child) should adjust
+ * their logic to accommodate only the velocity left.
+ *
+ * 4. Post-fling. Pass that happens after the scrolling descendant stopped flinging and wants to
+ * notify ancestors about that fact, providing velocity left to consume as a part of this. This
+ * pass should happen after the fling itself happens on the scrolling child. Ancestors of the
+ * dispatching node will have opportunity to fling themselves with the `velocityLeft` provided.
+ * Parent must call `notifySelfFinish` callback in order to continue the propagation of the
+ * velocity that is left to ancestors above.
+ *
+ * Example of the nested scrolling interaction where component both dispatches and consumed
+ * children's delta:
+ * @sample androidx.compose.ui.samples.NestedScrollSample
+ *
+ * @param connection connection to the nested scroll system to participate in the event chaining,
+ * receiving events when scrollable descendant is being scrolled.
+ * @param dispatcher object to be attached to the nested scroll system on which `dispatch*`
+ * methods can be called to notify ancestors within nested scroll system about scrolling happening
+ */
+fun Modifier.nestedScroll(
+    connection: NestedScrollConnection,
+    dispatcher: NestedScrollDispatcher? = null
+): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "nestedScroll"
+        properties["connection"] = connection
+        properties["dispatcher"] = dispatcher
+    }
+) {
+    // provide noop dispatcher if needed
+    val resolvedDispatcher = dispatcher ?: remember { NestedScrollDispatcher() }
+    remember(connection, resolvedDispatcher) {
+        object : NestedScrollModifier {
+            override val dispatcher: NestedScrollDispatcher = resolvedDispatcher
+            override val connection: NestedScrollConnection = connection
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt
index 7ea0160..8d43c717 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.gesture.scrollorientationlocking
 
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.CustomEvent
 import androidx.compose.ui.input.pointer.CustomEventDispatcher
 import androidx.compose.ui.input.pointer.PointerEventPass
@@ -46,7 +45,6 @@
  * [onCancel] to use
  * this correctly.
  */
-@ExperimentalPointerInput
 class ScrollOrientationLocker(private val customEventDispatcher: CustomEventDispatcher) {
 
     private var locker: InternalScrollOrientationLocker? = null
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
index f65a27d..2078c8b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
@@ -113,10 +113,14 @@
 }
 
 class VectorApplier(root: VNode) : AbstractApplier<VNode>(root) {
-    override fun insert(index: Int, instance: VNode) {
+    override fun insertTopDown(index: Int, instance: VNode) {
         current.asGroup().insertAt(index, instance)
     }
 
+    override fun insertBottomUp(index: Int, instance: VNode) {
+        // Ignored as the tree is built top-down.
+    }
+
     override fun remove(index: Int, count: Int) {
         current.asGroup().remove(index, count)
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/ExperimentalKeyInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/ExperimentalKeyInput.kt
deleted file mode 100644
index f176c30..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/ExperimentalKeyInput.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.input.key
-
-@RequiresOptIn("The Key Input API is experimental and is likely to change in the future.")
-annotation class ExperimentalKeyInput
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt
index fea9b39..69174ed6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt
@@ -21,7 +21,6 @@
  *
  * @param keyCode an integer code representing the key pressed.
  */
-@ExperimentalKeyInput
 expect inline class Key(val keyCode: Int) {
     companion object {
         /** Unknown key. */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt
index 47f001c..b5e4fa0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt
@@ -20,7 +20,6 @@
  * When a user presses a key on a hardware keyboard, a [KeyEvent] is sent to the
  * [KeyInputModifier] that is currently active.
  */
-@ExperimentalKeyInput
 interface KeyEvent {
     /**
      * The key that was pressed.
@@ -69,23 +68,11 @@
      * Indicates whether the Shift key is pressed.
      */
     val isShiftPressed: Boolean
-
-    /**
-     * Indicates the status of the Alt key.
-     */
-    @Suppress("DEPRECATION")
-    @Deprecated(
-        "alt is replaced by isAltPressed",
-        ReplaceWith("isAltPressed"),
-        DeprecationLevel.ERROR
-    )
-    val alt: Alt
 }
 
 /**
  * The type of Key Event.
  */
-@ExperimentalKeyInput
 enum class KeyEventType {
     /**
      * Unknown key event.
@@ -102,29 +89,3 @@
      */
     KeyDown
 }
-
-/**
- * Indicates the status of the Alt key.
- */
-@Deprecated(
-    message = "Alt is replaced by KeyEvent.isAltPressed",
-    level = DeprecationLevel.WARNING
-)
-@ExperimentalKeyInput
-interface Alt {
-    /**
-     * Indicates whether the Alt key is pressed.
-     */
-    val isPressed: Boolean
-        get() = isLeftAltPressed || isRightAltPressed
-
-    /**
-     * Indicates whether the left Alt key is pressed.
-     */
-    val isLeftAltPressed: Boolean
-
-    /**
-     * Indicates whether the right Alt key is pressed.
-     */
-    val isRightAltPressed: Boolean
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index dc186975..89dade5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -21,15 +21,23 @@
 import androidx.compose.ui.node.ModifiedKeyInputNode
 import androidx.compose.ui.platform.debugInspectorInfo
 
+// TODO(b/175156387): Remove this modifier after Alpha09
 /**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
  * allow it to intercept hardware key events.
  *
  * @param onKeyEvent This callback is invoked when the user interacts with the hardware keyboard.
  * While implementing this callback, return true to stop propagation of this event. If you return
- * false, the key event will be sent to this [keyInputFilter]'s parent.
+ * false, the key event will be sent to this [KeyInputModifier]'s parent.
  */
-@ExperimentalKeyInput
+@Deprecated(
+    message = "Use Modifier.onKeyEvent() instead",
+    replaceWith = ReplaceWith(
+        "onKeyEvent(onKeyEvent)",
+        "androidx.compose.ui.input.key.onKeyEvent"
+    ),
+    level = DeprecationLevel.ERROR
+)
 fun Modifier.keyInputFilter(onKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
     inspectorInfo = debugInspectorInfo {
         name = "keyInputFilter"
@@ -39,6 +47,51 @@
     KeyInputModifier(onKeyEvent = onKeyEvent, onPreviewKeyEvent = null)
 }
 
+// TODO(b/175156387): Remove this modifier after Alpha09
+/**
+ * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
+ * allow it to intercept hardware key events.
+ *
+ * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
+ * Return true to stop propagation of this event. If you return false, the key event will be sent
+ * to this [previewKeyInputFilter]'s child. If none of the children consume the event, it will be
+ * sent back up to the root [KeyInputModifier] using the onKeyEvent callback.
+ */
+@Deprecated(
+    message = "Use Modifier.onPreviewKeyEvent() instead",
+    replaceWith = ReplaceWith(
+        "onPreviewKeyEvent(onPreviewKeyEvent)",
+        "androidx.compose.ui.input.key.onPreviewKeyEvent"
+    ),
+    level = DeprecationLevel.ERROR
+)
+fun Modifier.previewKeyInputFilter(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "previewKeyInputFilter"
+        properties["onPreviewKeyEvent"] = onPreviewKeyEvent
+    }
+) {
+    KeyInputModifier(onKeyEvent = null, onPreviewKeyEvent = onPreviewKeyEvent)
+}
+
+/**
+ * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
+ * allow it to intercept hardware key events.
+ *
+ * @param onKeyEvent This callback is invoked when the user interacts with the hardware keyboard.
+ * While implementing this callback, return true to stop propagation of this event. If you return
+ * false, the key event will be sent to this [onKeyEvent]'s parent.
+ */
+fun Modifier.onKeyEvent(onKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "onKeyEvent"
+        properties["onKeyEvent"] = onKeyEvent
+    }
+) {
+    KeyInputModifier(onKeyEvent = onKeyEvent, onPreviewKeyEvent = null)
+}
+
 /**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
  * allow it to intercept hardware key events.
@@ -46,20 +99,18 @@
  * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
  * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
  * Return true to stop propagation of this event. If you return false, the key event will be sent
- * to this [previewKeyInputFilter]'s child. If none of the children consume the event, it will be
- * sent back up to the root [keyInputFilter] using the onKeyEvent callback.
+ * to this [onPreviewKeyEvent]'s child. If none of the children consume the event, it will be
+ * sent back up to the root [KeyInputModifier] using the onKeyEvent callback.
  */
-@ExperimentalKeyInput
-fun Modifier.previewKeyInputFilter(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
+fun Modifier.onPreviewKeyEvent(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
     inspectorInfo = debugInspectorInfo {
-        name = "previewKeyInputFilter"
+        name = "onPreviewKeyEvent"
         properties["onPreviewKeyEvent"] = onPreviewKeyEvent
     }
 ) {
     KeyInputModifier(onKeyEvent = null, onPreviewKeyEvent = onPreviewKeyEvent)
 }
 
-@OptIn(ExperimentalKeyInput::class)
 internal class KeyInputModifier(
     val onKeyEvent: ((KeyEvent) -> Boolean)?,
     val onPreviewKeyEvent: ((KeyEvent) -> Boolean)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index ebd129a..6738ed7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.input.pointer
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.util.fastForEach
@@ -24,7 +23,7 @@
 /**
  * The core element that receives [PointerInputEvent]s and process them in Compose UI.
  */
-@OptIn(ExperimentalLayoutNodeApi::class, InternalCoreApi::class)
+@OptIn(InternalCoreApi::class)
 internal class PointerInputEventProcessor(val root: LayoutNode) {
 
     private val hitPathTracker = HitPathTracker()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 6f534f2..aac7f19 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -21,7 +21,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientViewConfiguration
 import androidx.compose.ui.platform.ViewConfiguration
@@ -29,7 +28,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastMapTo
+import kotlinx.coroutines.CancellableContinuation
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.ContinuationInterceptor
@@ -39,17 +38,19 @@
 import kotlin.coroutines.createCoroutine
 import kotlin.coroutines.resume
 
+@Deprecated("Use AwaitPointerEventScope", ReplaceWith("AwaitPointerEventScope"))
+typealias HandlePointerInputScope = AwaitPointerEventScope
+
 /**
- * Receiver scope for awaiting pointer events in a call to [PointerInputScope.handlePointerInput].
+ * Receiver scope for awaiting pointer events in a call to [PointerInputScope.awaitPointerEventScope].
  *
  * This is a restricted suspension scope. Code in this scope is always called undispatched and
- * may only suspend for calls to [awaitPointerEvent] or [awaitCustomEvent]. These functions
+ * may only suspend for calls to [awaitPointerEvent]. These functions
  * resume synchronously and the caller may mutate the result **before** the next await call to
  * affect the next stage of the input processing pipeline.
  */
-@ExperimentalPointerInput
 @RestrictsSuspension
-interface HandlePointerInputScope : Density {
+interface AwaitPointerEventScope : Density {
     /**
      * The measured size of the pointer input region. Input events will be reported with
      * a coordinate space of (0, 0) to (size.width, size,height) as the input region, with
@@ -58,9 +59,9 @@
     val size: IntSize
 
     /**
-     * The state of the pointers as of the most recent event
+     * The [PointerEvent] from the most recent touch event.
      */
-    val currentPointers: List<PointerInputData>
+    val currentEvent: PointerEvent
 
     /**
      * The [ViewConfiguration] used to tune gesture detectors.
@@ -80,25 +81,11 @@
     suspend fun awaitPointerEvent(
         pass: PointerEventPass = PointerEventPass.Main
     ): PointerEvent
-
-    /**
-     * Suspend until a [CustomEvent] is reported to the specified input [pass].
-     * [pass] defaults to [PointerEventPass.Main].
-     *
-     * [awaitCustomEvent] resumes **synchronously** in the restricted suspension scope. This
-     * means that callers can react immediately to input after [awaitCustomEvent] returns
-     * and affect both the current frame and the next handler or phase of the input processing
-     * pipeline. Callers should mutate the returned [CustomEvent] before awaiting
-     * another event to consume aspects of the event before the next stage of input processing runs.     */
-    suspend fun awaitCustomEvent(
-        pass: PointerEventPass = PointerEventPass.Main
-    ): CustomEvent
 }
 
 /**
  * Receiver scope for [Modifier.pointerInput] that permits
- * [handling pointer input][handlePointerInput] and
- * [sending custom input events][customEventDispatcher].
+ * [handling pointer input][awaitPointerEventScope].
  */
 // Design note: this interface does _not_ implement CoroutineScope, even though doing so
 // would more easily permit the use of launch {} inside Modifier.pointerInput {} blocks without
@@ -106,7 +93,6 @@
 // gesture detectors as suspending extensions with a PointerInputScope receiver, also making this
 // interface implement CoroutineScope would be an invitation to break structured concurrency in
 // these extensions, leaving other launched coroutines running in the calling scope.
-@ExperimentalPointerInput
 interface PointerInputScope : Density {
     /**
      * The measured size of the pointer input region. Input events will be reported with
@@ -116,40 +102,37 @@
     val size: IntSize
 
     /**
-     * [customEventDispatcher] permits dispatching custom input events to the rest of the UI
-     * in response to handling lower-level pointer input events. Accessing [customEventDispatcher]
-     * before the first pointer input event is reported will throw [IllegalStateException].
-     */
-    val customEventDispatcher: CustomEventDispatcher
-
-    /**
      * The [ViewConfiguration] used to tune gesture detectors.
      */
     val viewConfiguration: ViewConfiguration
 
     /**
-     * Suspend and install a pointer input [handler] that can await input events and respond to
-     * them immediately. A call to [handlePointerInput] will resume with [handler]'s result after
+     * Suspend and install a pointer input [block] that can await input events and respond to
+     * them immediately. A call to [awaitPointerEventScope] will resume with [block]'s result after
      * it completes.
      *
-     * More than one [handlePointerInput] can run concurrently in the same [PointerInputScope] by
-     * using [kotlinx.coroutines.launch]. Handlers are dispatched to in the order in which they
+     * More than one [awaitPointerEventScope] can run concurrently in the same [PointerInputScope] by
+     * using [kotlinx.coroutines.launch]. [block]s are dispatched to in the order in which they
      * were installed.
      */
-    suspend fun <R> handlePointerInput(
-        handler: suspend HandlePointerInputScope.() -> R
+    suspend fun <R> awaitPointerEventScope(
+        block: suspend AwaitPointerEventScope.() -> R
     ): R
+
+    @Deprecated("Use awaitPointerEventScope", ReplaceWith("awaitPointerEventScope(handler)"))
+    suspend fun <R> handlePointerInput(
+        handler: suspend AwaitPointerEventScope.() -> R
+    ): R = awaitPointerEventScope(handler)
 }
 
 /**
  * Create a modifier for processing pointer input within the region of the modified element.
  *
- * [pointerInput] [block]s may call [PointerInputScope.handlePointerInput] to install a pointer
- * input handler that can [HandlePointerInputScope.awaitPointerEvent] to receive and consume
- * pointer input events. Extension functions on [PointerInputScope] or [HandlePointerInputScope]
+ * [pointerInput] [block]s may call [PointerInputScope.awaitPointerEventScope] to install a pointer
+ * input handler that can [AwaitPointerEventScope.awaitPointerEvent] to receive and consume
+ * pointer input events. Extension functions on [PointerInputScope] or [AwaitPointerEventScope]
  * may be defined to perform higher-level gesture detection.
  */
-@ExperimentalPointerInput
 fun Modifier.pointerInput(
     block: suspend PointerInputScope.() -> Unit
 ): Modifier = composed(
@@ -177,14 +160,13 @@
  * a LayoutNode.
  *
  * [SuspendingPointerInputFilter] implements the [PointerInputScope] used to offer the
- * [Modifier.pointerInput] DSL and carries the [Density] from [DensityAmbient] at the point of
+ * [Modifier.pointerInput] DSL and carries the [Density] from [AmbientDensity] at the point of
  * the modifier's materialization. Even if this value were returned to the [PointerInputFilter]
  * callbacks, we would still need the value at composition time in order for [Modifier.pointerInput]
  * to begin its internal [LaunchedEffect] for the provided code block.
  */
 // TODO: Suppressing deprecation for synchronized; need to move to atomicfu wrapper
 @Suppress("DEPRECATION_ERROR")
-@ExperimentalPointerInput
 internal class SuspendingPointerInputFilter(
     override val viewConfiguration: ViewConfiguration,
     density: Density = Density(1f)
@@ -196,25 +178,13 @@
     override val pointerInputFilter: PointerInputFilter
         get() = this
 
-    private var _customEventDispatcher: CustomEventDispatcher? = null
-
-    val currentPointers = mutableListOf<PointerInputData>()
-
-    /**
-     * TODO: work out whether this is actually a race or not.
-     * It shouldn't be, as we will have attached the [PointerInputModifier] during
-     * composition-apply by the time the [LaunchedEffect] that would access this property
-     * is dispatched and begins running.
-     */
-    override val customEventDispatcher: CustomEventDispatcher
-        get() = _customEventDispatcher ?: error("customEventDispatcher not yet available")
+    private var currentEvent: PointerEvent? = null
 
     override fun onInit(customEventDispatcher: CustomEventDispatcher) {
-        _customEventDispatcher = customEventDispatcher
     }
 
     /**
-     * Actively registered input handlers from currently ongoing calls to [handlePointerInput].
+     * Actively registered input handlers from currently ongoing calls to [awaitPointerEventScope].
      * Must use `synchronized(pointerHandlers)` to access.
      */
     private val pointerHandlers = mutableVectorOf<PointerEventHandlerCoroutine<*>>()
@@ -236,6 +206,13 @@
     private var lastPointerEvent: PointerEvent? = null
 
     /**
+     * The size of the bounds of this input filter. Normally [PointerInputFilter.size] can
+     * be used, but for tests, it is better to not rely on something set to an `internal`
+     * method.
+     */
+    private var boundsSize: IntSize = IntSize.Zero
+
+    /**
      * Snapshot the current [pointerHandlers] and run [block] on each one.
      * May not be called reentrant or concurrent with itself.
      *
@@ -255,13 +232,9 @@
         try {
             when (pass) {
                 PointerEventPass.Initial, PointerEventPass.Final ->
-                    dispatchingPointerHandlers.forEach {
-                        block(it)
-                    }
+                    dispatchingPointerHandlers.forEach(block)
                 PointerEventPass.Main ->
-                    dispatchingPointerHandlers.forEachReversed {
-                        block(it)
-                    }
+                    dispatchingPointerHandlers.forEachReversed(block)
             }
         } finally {
             dispatchingPointerHandlers.clear()
@@ -286,9 +259,9 @@
         pass: PointerEventPass,
         bounds: IntSize
     ) {
+        boundsSize = bounds
         if (pass == PointerEventPass.Initial) {
-            currentPointers.clear()
-            pointerEvent.changes.fastMapTo(currentPointers) { it.current }
+            currentEvent = pointerEvent
         }
         dispatchPointerEvent(pointerEvent, pass)
 
@@ -325,21 +298,12 @@
     }
 
     override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {
-        forEachCurrentPointerHandler(pass) {
-            it.offerCustomEvent(customEvent, pass)
-        }
     }
 
-    override suspend fun <R> handlePointerInput(
-        handler: suspend HandlePointerInputScope.() -> R
+    override suspend fun <R> awaitPointerEventScope(
+        block: suspend AwaitPointerEventScope.() -> R
     ): R = suspendCancellableCoroutine { continuation ->
-        val handlerCoroutine = PointerEventHandlerCoroutine(
-            continuation,
-            currentPointers,
-            size,
-            viewConfiguration,
-            this
-        )
+        val handlerCoroutine = PointerEventHandlerCoroutine(continuation)
         synchronized(pointerHandlers) {
             pointerHandlers += handlerCoroutine
 
@@ -356,29 +320,37 @@
             // behavior in our restricted suspension scope. This is required so that we can
             // process event-awaits synchronously and affect the next stage in the pipeline
             // without running too late due to dispatch.
-            handler.createCoroutine(handlerCoroutine, handlerCoroutine).resume(Unit)
+            block.createCoroutine(handlerCoroutine, handlerCoroutine).resume(Unit)
         }
+
+        // Restricted suspension handler coroutines can't propagate structured job cancellation
+        // automatically as the context must be EmptyCoroutineContext; do it manually instead.
+        continuation.invokeOnCancellation { handlerCoroutine.cancel(it) }
     }
 
     /**
      * Implementation of the inner coroutine created to run a single call to
-     * [handlePointerInput].
+     * [awaitPointerEventScope].
      *
-     * [PointerEventHandlerCoroutine] implements [HandlePointerInputScope] to provide the
+     * [PointerEventHandlerCoroutine] implements [AwaitPointerEventScope] to provide the
      * input handler DSL, and [Continuation] so that it can wrap [completion] and remove the
      * [ContinuationInterceptor] from the calling context and run undispatched.
      */
     private inner class PointerEventHandlerCoroutine<R>(
         private val completion: Continuation<R>,
-        override val currentPointers: List<PointerInputData>,
-        override val size: IntSize,
-        override val viewConfiguration: ViewConfiguration,
-        density: Density
-    ) : HandlePointerInputScope, Density by density, Continuation<R> {
-        private var pointerAwaiter: Continuation<PointerEvent>? = null
-        private var customAwaiter: Continuation<CustomEvent>? = null
+    ) : AwaitPointerEventScope, Density by this@SuspendingPointerInputFilter, Continuation<R> {
+        private var pointerAwaiter: CancellableContinuation<PointerEvent>? = null
         private var awaitPass: PointerEventPass = PointerEventPass.Main
 
+        override val currentEvent: PointerEvent
+            get() = checkNotNull(this@SuspendingPointerInputFilter.currentEvent) {
+                "cannot access currentEvent outside of input dispatch"
+            }
+        override val size: IntSize
+            get() = this@SuspendingPointerInputFilter.boundsSize
+        override val viewConfiguration: ViewConfiguration
+            get() = this@SuspendingPointerInputFilter.viewConfiguration
+
         fun offerPointerEvent(event: PointerEvent, pass: PointerEventPass) {
             if (pass == awaitPass) {
                 pointerAwaiter?.run {
@@ -388,17 +360,14 @@
             }
         }
 
-        fun offerCustomEvent(event: CustomEvent, pass: PointerEventPass) {
-            if (pass == awaitPass) {
-                customAwaiter?.run {
-                    customAwaiter = null
-                    resume(event)
-                }
-            }
+        // Called to run any finally blocks in the awaitPointerEventScope block
+        fun cancel(cause: Throwable?) {
+            pointerAwaiter?.cancel(cause)
+            pointerAwaiter = null
         }
 
-        override val context: CoroutineContext =
-            EmptyCoroutineContext
+        // context must be EmptyCoroutineContext for restricted suspension coroutines
+        override val context: CoroutineContext = EmptyCoroutineContext
 
         // Implementation of Continuation; clean up and resume our wrapped continuation.
         override fun resumeWith(result: Result<R>) {
@@ -414,12 +383,5 @@
             awaitPass = pass
             pointerAwaiter = continuation
         }
-
-        override suspend fun awaitCustomEvent(
-            pass: PointerEventPass
-        ): CustomEvent = suspendCancellableCoroutine { continuation ->
-            awaitPass = pass
-            customAwaiter = continuation
-        }
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index 628d7f4..675a0a0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -29,9 +29,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.materialize
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutEmitHelper
 import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.MeasureBlocks
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.platform.simpleIdentityToString
@@ -80,7 +80,6 @@
  */
 @Suppress("ComposableLambdaParameterPosition")
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class)
 fun Layout(
     content: @Composable () -> Unit,
     minIntrinsicWidthMeasureBlock: IntrinsicMeasureBlock,
@@ -101,7 +100,7 @@
 }
 
 /**
- * Creates an instance of [LayoutNode.MeasureBlocks] to pass to [Layout] given
+ * Creates an instance of [MeasureBlocks] to pass to [Layout] given
  * intrinsic measures and a measure block.
  *
  * @sample androidx.compose.ui.samples.LayoutWithMeasureBlocksWithIntrinsicUsage
@@ -130,15 +129,14 @@
  * @see Layout
  * @see WithConstraints
  */
-@ExperimentalLayoutNodeApi
 fun measureBlocksOf(
     minIntrinsicWidthMeasureBlock: IntrinsicMeasureBlock,
     minIntrinsicHeightMeasureBlock: IntrinsicMeasureBlock,
     maxIntrinsicWidthMeasureBlock: IntrinsicMeasureBlock,
     maxIntrinsicHeightMeasureBlock: IntrinsicMeasureBlock,
     measureBlock: MeasureBlock
-): LayoutNode.MeasureBlocks {
-    return object : LayoutNode.MeasureBlocks {
+): MeasureBlocks {
+    return object : MeasureBlocks {
         override fun measure(
             measureScope: MeasureScope,
             measurables: List<Measurable>,
@@ -191,7 +189,6 @@
  */
 @Suppress("ComposableLambdaParameterPosition")
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class)
 /*inline*/ fun Layout(
     /*crossinline*/
     content: @Composable () -> Unit,
@@ -218,7 +215,7 @@
  *
  * @param content The children composable to be laid out.
  * @param modifier Modifiers to be applied to the layout.
- * @param measureBlocks An [LayoutNode.MeasureBlocks] instance defining the measurement and
+ * @param measureBlocks An [MeasureBlocks] instance defining the measurement and
  * positioning of the layout.
  *
  * @see Layout
@@ -227,10 +224,9 @@
  */
 
 @Suppress("ComposableLambdaParameterPosition")
-@ExperimentalLayoutNodeApi
 @Composable inline fun Layout(
     content: @Composable () -> Unit,
-    measureBlocks: LayoutNode.MeasureBlocks,
+    measureBlocks: MeasureBlocks,
     modifier: Modifier = Modifier
 ) {
     @OptIn(ExperimentalComposeApi::class)
@@ -246,7 +242,6 @@
     )
 }
 
-@ExperimentalLayoutNodeApi
 @PublishedApi
 internal fun materializerOf(
     modifier: Modifier
@@ -263,7 +258,6 @@
     "This composable is temporary to enable quicker prototyping in ConstraintLayout. " +
         "It should not be used in app code directly."
 )
-@OptIn(ExperimentalLayoutNodeApi::class)
 fun MultiMeasureLayout(
     modifier: Modifier = Modifier,
     children: @Composable () -> Unit,
@@ -374,19 +368,17 @@
  * call.
  */
 @PublishedApi
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class IntrinsicsMeasureScope(
     density: Density,
     override val layoutDirection: LayoutDirection
 ) : MeasureScope, Density by density
 
 /**
- * Default [LayoutNode.MeasureBlocks] object implementation, providing intrinsic measurements
+ * Default [MeasureBlocks] object implementation, providing intrinsic measurements
  * that use the measure block replacing the measure calls with intrinsic measurement calls.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 fun MeasuringIntrinsicsMeasureBlocks(measureBlock: MeasureBlock) =
-    object : LayoutNode.MeasureBlocks {
+    object : MeasureBlocks {
         override fun measure(
             measureScope: MeasureScope,
             measurables: List<Measurable>,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
new file mode 100644
index 0000000..14f8ce2
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.ui.Modifier
+
+/**
+ * The public information about the layouts used internally as nodes in the Compose UI hierarchy.
+ */
+interface LayoutInfo {
+
+    /**
+     * This returns a new List of [Modifier]s and the coordinates and any extra information
+     * that may be useful. This is used for tooling to retrieve layout modifier and layer
+     * information.
+     */
+    fun getModifierInfo(): List<ModifierInfo>
+
+    /**
+     * The measured width of this layout and all of its modifiers.
+     */
+    val width: Int
+
+    /**
+     * The measured height of this layout and all of its modifiers.
+     */
+    val height: Int
+
+    /**
+     * Coordinates of just the contents of the layout, after being affected by all modifiers.
+     */
+    val coordinates: LayoutCoordinates
+
+    /**
+     * Whether or not this layout and all of its parents have been placed in the hierarchy.
+     */
+    val isPlaced: Boolean
+
+    /**
+     * Parent of this layout.
+     */
+    val parentInfo: LayoutInfo?
+
+    /**
+     * Returns true if this layout is currently a part of the layout tree.
+     */
+    val isAttached: Boolean
+}
+
+/**
+ * Used by tooling to examine the modifiers on a [LayoutInfo].
+ */
+class ModifierInfo(
+    val modifier: Modifier,
+    val coordinates: LayoutCoordinates,
+    val extra: Any? = null
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasureBlocks.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasureBlocks.kt
index e33b7e9..6a74225 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasureBlocks.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/RootMeasureBlocks.kt
@@ -16,14 +16,12 @@
 
 package androidx.compose.ui.layout
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.util.fastForEach
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal object RootMeasureBlocks : LayoutNode.NoIntrinsicsMeasureBlocks(
     "Undefined intrinsics block and it is required"
 ) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 99741dd..53805c9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -29,17 +29,15 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.materialize
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutEmitHelper
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.LayoutState
-import androidx.compose.ui.node.isAttached
+import androidx.compose.ui.node.MeasureBlocks
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.platform.subcomposeInto
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
 
 /**
  * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
@@ -60,7 +58,7 @@
  * @param measureBlock Measure block which provides ability to subcompose during the measuring.
  */
 @Composable
-@OptIn(ExperimentalLayoutNodeApi::class, ExperimentalComposeApi::class)
+@OptIn(ExperimentalComposeApi::class)
 fun SubcomposeLayout(
     modifier: Modifier = Modifier,
     measureBlock: SubcomposeMeasureScope.(Constraints) -> MeasureResult
@@ -79,8 +77,6 @@
             set(AmbientLayoutDirection.current, LayoutEmitHelper.setLayoutDirection)
         }
     )
-
-    state.subcomposeIfRemeasureNotScheduled()
 }
 
 /**
@@ -102,7 +98,6 @@
     fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable>
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 private class SubcomposeLayoutState :
     SubcomposeMeasureScope,
     CompositionLifecycleObserver {
@@ -160,15 +155,6 @@
         return node.children
     }
 
-    fun subcomposeIfRemeasureNotScheduled() {
-        val root = root!!
-        if (root.layoutState != LayoutState.NeedsRemeasure && root.isAttached()) {
-            root.foldedChildren.fastForEach {
-                subcompose(it, nodeToNodeState.getValue(it))
-            }
-        }
-    }
-
     private fun subcompose(node: LayoutNode, nodeState: NodeState) {
         node.ignoreModelReads {
             val content = nodeState.content
@@ -196,7 +182,7 @@
 
     private fun createMeasureBlocks(
         block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
-    ): LayoutNode.MeasureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks(
+    ): MeasureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks(
         error = "Intrinsic measurements are not currently supported by SubcomposeLayout"
     ) {
         override fun measure(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/TestModifierUpdater.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/TestModifierUpdater.kt
new file mode 100644
index 0000000..ce35d16
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/TestModifierUpdater.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.runtime.Applier
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.emit
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.LayoutEmitHelper
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.util.annotation.VisibleForTesting
+
+/** @hide */
+@Deprecated(
+    "It is a test API, do not use it in the real applications",
+    level = DeprecationLevel.ERROR
+)
+@VisibleForTesting
+class TestModifierUpdater internal constructor(private val node: LayoutNode) {
+    fun updateModifier(modifier: Modifier) {
+        node.modifier = modifier
+    }
+}
+
+/** @hide */
+@Deprecated(
+    "It is a test API, do not use it in the real applications",
+    level = DeprecationLevel.ERROR
+)
+@VisibleForTesting
+@Composable
+@Suppress("DEPRECATION_ERROR")
+fun TestModifierUpdaterLayout(onAttached: (TestModifierUpdater) -> Unit) {
+    val measureBlocks = MeasuringIntrinsicsMeasureBlocks { _, constraints ->
+        layout(constraints.maxWidth, constraints.maxHeight) {}
+    }
+    emit<LayoutNode, Applier<Any>>(
+        ctor = LayoutEmitHelper.constructor,
+        update = {
+            set(measureBlocks, LayoutEmitHelper.setMeasureBlocks)
+            set(Unit) { onAttached(TestModifierUpdater(this)) }
+        }
+    )
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index 9d6bd30..e786f46 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -17,8 +17,6 @@
 package androidx.compose.ui.node
 
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.GraphicsLayerScope
@@ -34,7 +32,6 @@
 /**
  * [LayoutNodeWrapper] with default implementations for methods.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal open class DelegatingLayoutNodeWrapper<T : Modifier.Element>(
     override var wrapped: LayoutNodeWrapper,
     open var modifier: T
@@ -49,6 +46,10 @@
      */
     var isChained = false
 
+    // This is used by LayoutNode to mark LayoutNodeWrappers that are going to be reused
+    // because they match the modifier instance.
+    var toBeReusedForSameModifier = false
+
     init {
         wrapped.wrappedBy = this
     }
@@ -133,10 +134,9 @@
         return lastFocusWrapper
     }
 
-    @OptIn(ExperimentalFocus::class)
-    override fun propagateFocusStateChange(focusState: FocusState) {
-        wrappedBy?.propagateFocusStateChange(focusState)
-    }
+    override fun findPreviousNestedScrollWrapper() = wrappedBy?.findPreviousNestedScrollWrapper()
+
+    override fun findNextNestedScrollWrapper() = wrapped.findNextNestedScrollWrapper()
 
     override fun findPreviousKeyInputWrapper() = wrappedBy?.findPreviousKeyInputWrapper()
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt
index dbf3b46..d439d47 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt
@@ -28,7 +28,6 @@
  * as any of this modifications can break the comparator's contract which can cause
  * to not find the item in the tree set, which we previously added.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class DepthSortedSet(
     private val extraAssertions: Boolean = true
 ) {
@@ -59,7 +58,7 @@
     }
 
     fun add(node: LayoutNode) {
-        check(node.isAttached())
+        check(node.isAttached)
         if (extraAssertions) {
             val usedDepth = mapOfOriginalDepth[node]
             if (usedDepth == null) {
@@ -72,7 +71,7 @@
     }
 
     fun remove(node: LayoutNode) {
-        check(node.isAttached())
+        check(node.isAttached)
         val contains = set.remove(node)
         if (extraAssertions) {
             val usedDepth = mapOfOriginalDepth.remove(node)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ExperimentalLayoutNodeApi.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ExperimentalLayoutNodeApi.kt
deleted file mode 100644
index fd9d914..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ExperimentalLayoutNodeApi.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-@RequiresOptIn(
-    level = RequiresOptIn.Level.ERROR,
-    message = "This is an experimental API for Compose UI LayoutNode and is likely to change " +
-        "before becoming stable."
-)
-@Target(
-    AnnotationTarget.CLASS,
-    AnnotationTarget.FUNCTION,
-    AnnotationTarget.PROPERTY
-)
-annotation class ExperimentalLayoutNodeApi
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index 23a7493..0bd0279 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -16,9 +16,8 @@
 
 package androidx.compose.ui.node
 
-import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDelegatingWrapper
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.GraphicsLayerScope
@@ -31,7 +30,6 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class InnerPlaceable(
     layoutNode: LayoutNode
 ) : LayoutNodeWrapper(layoutNode), Density by layoutNode.measureScope {
@@ -60,13 +58,12 @@
 
     override fun findLastFocusWrapper(): ModifiedFocusNode? = findPreviousFocusWrapper()
 
-    @OptIn(ExperimentalFocus::class)
-    override fun propagateFocusStateChange(focusState: FocusState) {
-        wrappedBy?.propagateFocusStateChange(focusState)
-    }
-
     override fun findPreviousKeyInputWrapper() = wrappedBy?.findPreviousKeyInputWrapper()
 
+    override fun findPreviousNestedScrollWrapper() = wrappedBy?.findPreviousNestedScrollWrapper()
+
+    override fun findNextNestedScrollWrapper(): NestedScrollDelegatingWrapper? = null
+
     override fun findNextKeyInputWrapper(): ModifiedKeyInputNode? = null
 
     override fun findLastKeyInputWrapper(): ModifiedKeyInputNode? = findPreviousKeyInputWrapper()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 0470a3f..494c14a7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -17,19 +17,15 @@
 
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.ui.ContentDrawScope
-import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.FocusModifier
-import androidx.compose.ui.FocusObserverModifier
 import androidx.compose.ui.FocusRequesterModifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.draw.DrawModifier
+import androidx.compose.ui.focus.FocusEventModifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDelegatingWrapper
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollModifier
 import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.input.key.KeyInputModifier
 import androidx.compose.ui.input.pointer.PointerInputFilter
 import androidx.compose.ui.input.pointer.PointerInputModifier
@@ -38,10 +34,12 @@
 import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LayoutInfo
 import androidx.compose.ui.layout.LayoutModifier
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.ModifierInfo
 import androidx.compose.ui.layout.OnGloballyPositionedModifier
 import androidx.compose.ui.layout.OnRemeasuredModifier
 import androidx.compose.ui.layout.ParentDataModifier
@@ -78,14 +76,9 @@
 /**
  * An element in the layout hierarchy, built with compose UI.
  */
-@ExperimentalLayoutNodeApi
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalLayoutNodeApi::class
-)
-class LayoutNode : Measurable, Remeasurement, OwnerScope {
+class LayoutNode : Measurable, Remeasurement, OwnerScope, LayoutInfo {
 
-    constructor() : this(false)
+    internal constructor() : this(false)
 
     internal constructor(isVirtual: Boolean) {
         this.isVirtual = isVirtual
@@ -133,7 +126,7 @@
             unfoldedVirtualChildrenListDirty = true
         }
         if (isVirtual) {
-            parent?.unfoldedVirtualChildrenListDirty = true
+            this.parent?.unfoldedVirtualChildrenListDirty = true
         }
     }
 
@@ -148,13 +141,13 @@
     /**
      * The children of this LayoutNode, controlled by [insertAt], [move], and [removeAt].
      */
-    val children: List<LayoutNode> get() = _children.asMutableList()
+    internal val children: List<LayoutNode> get() = _children.asMutableList()
 
     /**
-     * The parent node in the LayoutNode hierarchy. This is `null` when the `LayoutNode`
-     * is attached (has an [owner]) and is the root of the tree or has not had [add] called for it.
+     * The parent node in the LayoutNode hierarchy. This is `null` when the [LayoutNode]
+     * is not attached attached to a hierarchy or is the root of the hierarchy.
      */
-    var parent: LayoutNode? = null
+    internal var parent: LayoutNode? = null
         get() {
             val parent = field
             return if (parent != null && parent.isVirtual) parent.parent else parent
@@ -164,13 +157,19 @@
     /**
      * The view system [Owner]. This `null` until [attach] is called
      */
-    var owner: Owner? = null
+    internal var owner: Owner? = null
         private set
 
     /**
-     * The tree depth of the LayoutNode. This is valid only when [owner] is not `null`.
+     * Returns true if this [LayoutNode] currently has an [LayoutNode.owner].  Semantically,
+     * this means that the LayoutNode is currently a part of a component tree.
      */
-    var depth: Int = 0
+    override val isAttached: Boolean get() = owner != null
+
+    /**
+     * The tree depth of the [LayoutNode]. This is valid only when it is attached to a hierarchy.
+     */
+    internal var depth: Int = 0
 
     /**
      * The layout state the node is currently in.
@@ -189,7 +188,7 @@
      * Inserts a child [LayoutNode] at a particular index. If this LayoutNode [owner] is not `null`
      * then [instance] will become [attach]ed also. [instance] must have a `null` [parent].
      */
-    fun insertAt(index: Int, instance: LayoutNode) {
+    internal fun insertAt(index: Int, instance: LayoutNode) {
         check(instance.parent == null) {
             "Cannot insert $instance because it already has a parent"
         }
@@ -222,7 +221,7 @@
     /**
      * Removes one or more children, starting at [index].
      */
-    fun removeAt(index: Int, count: Int) {
+    internal fun removeAt(index: Int, count: Int) {
         require(count >= 0) {
             "count ($count) must be greater than 0"
         }
@@ -249,7 +248,7 @@
     /**
      * Removes all children.
      */
-    fun removeAll() {
+    internal fun removeAll() {
         val attached = owner != null
         for (i in _foldedChildren.size - 1 downTo 0) {
             val child = _foldedChildren[i]
@@ -272,7 +271,7 @@
      * were LayoutNodes A B C D E, calling `move(1, 3, 1)` would result in the LayoutNodes
      * being reordered to A C B D E.
      */
-    fun move(from: Int, to: Int, count: Int) {
+    internal fun move(from: Int, to: Int, count: Int) {
         if (from == to) {
             return // nothing to do
         }
@@ -299,11 +298,11 @@
      * Set the [Owner] of this LayoutNode. This LayoutNode must not already be attached.
      * [owner] must match its [parent].[owner].
      */
-    fun attach(owner: Owner) {
+    internal fun attach(owner: Owner) {
         check(this.owner == null) {
             "Cannot attach $this as it already is attached"
         }
-        val parent = parent
+        val parent = this.parent
         check(parent == null || parent.owner == owner) {
             "Attaching to a different owner($owner) than the parent's owner(${parent?.owner})"
         }
@@ -327,7 +326,6 @@
         parent?.requestRemeasure()
         innerLayoutNodeWrapper.attach()
         forEachDelegate { it.attach() }
-        updateInnerLayerWrapper()
         onAttach?.invoke(owner)
     }
 
@@ -336,15 +334,15 @@
      * and its [parent]'s [owner] must be `null` before calling this. This will also [detach] all
      * children. After executing, the [owner] will be `null`.
      */
-    fun detach() {
+    internal fun detach() {
         val owner = owner
         checkNotNull(owner) {
             "Cannot detach node that is already detached!"
         }
-        val parentLayoutNode = parent
-        if (parentLayoutNode != null) {
-            parentLayoutNode.invalidateLayer()
-            parentLayoutNode.requestRemeasure()
+        val parent = this.parent
+        if (parent != null) {
+            parent.invalidateLayer()
+            parent.requestRemeasure()
         }
         alignmentLinesQueryOwner = null
         alignmentUsageByParent = UsageByParent.NotUsed
@@ -357,7 +355,6 @@
         }
         owner.onDetach(this)
         this.owner = null
-        _innerLayerWrapper = null
         depth = 0
         _foldedChildren.forEach { child ->
             child.detach()
@@ -389,7 +386,7 @@
         }
 
     override val isValid: Boolean
-        get() = isAttached()
+        get() = isAttached
 
     override fun toString(): String {
         return "${simpleIdentityToString(this, null)} children: ${children.size} " +
@@ -420,55 +417,7 @@
         return tree.toString()
     }
 
-    interface MeasureBlocks {
-        /**
-         * The function used to measure the child. It must call [MeasureScope.layout] before
-         * completing.
-         */
-        fun measure(
-            measureScope: MeasureScope,
-            measurables: List<Measurable>,
-            constraints: Constraints
-        ): MeasureResult
-
-        /**
-         * The function used to calculate [IntrinsicMeasurable.minIntrinsicWidth].
-         */
-        fun minIntrinsicWidth(
-            intrinsicMeasureScope: IntrinsicMeasureScope,
-            measurables: List<IntrinsicMeasurable>,
-            h: Int
-        ): Int
-
-        /**
-         * The lambda used to calculate [IntrinsicMeasurable.minIntrinsicHeight].
-         */
-        fun minIntrinsicHeight(
-            intrinsicMeasureScope: IntrinsicMeasureScope,
-            measurables: List<IntrinsicMeasurable>,
-            w: Int
-        ): Int
-
-        /**
-         * The function used to calculate [IntrinsicMeasurable.maxIntrinsicWidth].
-         */
-        fun maxIntrinsicWidth(
-            intrinsicMeasureScope: IntrinsicMeasureScope,
-            measurables: List<IntrinsicMeasurable>,
-            h: Int
-        ): Int
-
-        /**
-         * The lambda used to calculate [IntrinsicMeasurable.maxIntrinsicHeight].
-         */
-        fun maxIntrinsicHeight(
-            intrinsicMeasureScope: IntrinsicMeasureScope,
-            measurables: List<IntrinsicMeasurable>,
-            w: Int
-        ): Int
-    }
-
-    abstract class NoIntrinsicsMeasureBlocks(private val error: String) : MeasureBlocks {
+    internal abstract class NoIntrinsicsMeasureBlocks(private val error: String) : MeasureBlocks {
         override fun minIntrinsicWidth(
             intrinsicMeasureScope: IntrinsicMeasureScope,
             measurables: List<IntrinsicMeasurable>,
@@ -497,7 +446,7 @@
     /**
      * Blocks that define the measurement and intrinsic measurement of the layout.
      */
-    var measureBlocks: MeasureBlocks = ErrorMeasureBlocks
+    internal var measureBlocks: MeasureBlocks = ErrorMeasureBlocks
         set(value) {
             if (field != value) {
                 field = value
@@ -508,13 +457,13 @@
     /**
      * The screen density to be used by this layout.
      */
-    var density: Density = Density(1f)
+    internal var density: Density = Density(1f)
 
     /**
      * The scope used to run the [MeasureBlocks.measure]
      * [MeasureBlock][androidx.compose.ui.layout.MeasureBlock].
      */
-    val measureScope: MeasureScope = object : MeasureScope, Density {
+    internal val measureScope: MeasureScope = object : MeasureScope, Density {
         override val density: Float get() = this@LayoutNode.density.density
         override val fontScale: Float get() = this@LayoutNode.density.fontScale
         override val layoutDirection: LayoutDirection get() = this@LayoutNode.layoutDirection
@@ -534,12 +483,12 @@
     /**
      * The measured width of this layout and all of its [modifier]s. Shortcut for `size.width`.
      */
-    val width: Int get() = outerMeasurablePlaceable.width
+    override val width: Int get() = outerMeasurablePlaceable.width
 
     /**
      * The measured height of this layout and all of its [modifier]s. Shortcut for `size.height`.
      */
-    val height: Int get() = outerMeasurablePlaceable.height
+    override val height: Int get() = outerMeasurablePlaceable.height
 
     /**
      * The alignment lines of this layout, inherited + intrinsic
@@ -554,9 +503,9 @@
     internal val mDrawScope: LayoutNodeDrawScope = sharedDrawScope
 
     /**
-     * Whether or not this LayoutNode and all of its parents have been placed in the hierarchy.
+     * Whether or not this [LayoutNode] and all of its parents have been placed in the hierarchy.
      */
-    var isPlaced = false
+    override var isPlaced: Boolean = false
         private set
 
     /**
@@ -614,7 +563,7 @@
     private val previousAlignmentLines = mutableMapOf<AlignmentLine, Int>()
 
     @Deprecated("Temporary API to support ConstraintLayout prototyping.")
-    var canMultiMeasure: Boolean = false
+    internal var canMultiMeasure: Boolean = false
 
     internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
     private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
@@ -633,7 +582,20 @@
      * The inner-most layer wrapper. Used for performance for LayoutNodeWrapper.findLayer().
      */
     private var _innerLayerWrapper: LayoutNodeWrapper? = null
+    internal var innerLayerWrapperIsDirty = true
     internal val innerLayerWrapper: LayoutNodeWrapper? get() {
+        if (innerLayerWrapperIsDirty) {
+            var delegate: LayoutNodeWrapper? = innerLayoutNodeWrapper
+            val final = outerLayoutNodeWrapper.wrappedBy
+            _innerLayerWrapper = null
+            while (delegate != final) {
+                if (delegate?.layer != null) {
+                    _innerLayerWrapper = delegate
+                    break
+                }
+                delegate = delegate?.wrappedBy
+            }
+        }
         val layerWrapper = _innerLayerWrapper
         if (layerWrapper != null) {
             requireNotNull(layerWrapper.layer)
@@ -651,7 +613,7 @@
         if (innerLayerWrapper != null) {
             innerLayerWrapper.invalidateLayer()
         } else {
-            val parent = parent
+            val parent = this.parent
             parent?.invalidateLayer()
         }
     }
@@ -659,7 +621,7 @@
     /**
      * The [Modifier] currently applied to this node.
      */
-    var modifier: Modifier = Modifier
+    internal var modifier: Modifier = Modifier
         set(value) {
             if (value == field) return
             if (modifier != Modifier) {
@@ -670,10 +632,11 @@
             val invalidateParentLayer = shouldInvalidateParentLayer()
 
             copyWrappersToCache()
+            markReusedModifiers(value)
 
-            // Rebuild layoutNodeWrapper
+            // Rebuild LayoutNodeWrapper
             val oldOuterWrapper = outerMeasurablePlaceable.outerWrapper
-            if (outerSemantics != null && isAttached()) {
+            if (outerSemantics != null && isAttached) {
                 owner!!.onSemanticsChange()
             }
             val addedCallback = hasNewPositioningCallback()
@@ -708,8 +671,8 @@
                     if (mod is FocusModifier) {
                         wrapper = ModifiedFocusNode(wrapper, mod).assignChained(toWrap)
                     }
-                    if (mod is FocusObserverModifier) {
-                        wrapper = ModifiedFocusObserverNode(wrapper, mod).assignChained(toWrap)
+                    if (mod is FocusEventModifier) {
+                        wrapper = ModifiedFocusEventNode(wrapper, mod).assignChained(toWrap)
                     }
                     if (mod is FocusRequesterModifier) {
                         wrapper = ModifiedFocusRequesterNode(wrapper, mod).assignChained(toWrap)
@@ -720,6 +683,9 @@
                     if (mod is PointerInputModifier) {
                         wrapper = PointerInputDelegatingWrapper(wrapper, mod).assignChained(toWrap)
                     }
+                    if (mod is NestedScrollModifier) {
+                        wrapper = NestedScrollDelegatingWrapper(wrapper, mod).assignChained(toWrap)
+                    }
                     if (mod is LayoutModifier) {
                         wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
                     }
@@ -736,13 +702,10 @@
             outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
             outerMeasurablePlaceable.outerWrapper = outerWrapper
 
-            if (isAttached()) {
+            if (isAttached) {
                 // call detach() on all removed LayoutNodeWrappers
                 wrapperCache.forEach {
                     it.detach()
-                    if (_innerLayerWrapper === it) {
-                        _innerLayerWrapper = null
-                    }
                 }
 
                 // attach() all new LayoutNodeWrappers
@@ -782,21 +745,20 @@
         }
 
     /**
-     * Coordinates of just the contents of the LayoutNode, after being affected by all modifiers.
+     * Coordinates of just the contents of the [LayoutNode], after being affected by all modifiers.
      */
-    // TODO(mount): remove this
-    val coordinates: LayoutCoordinates
+    override val coordinates: LayoutCoordinates
         get() = innerLayoutNodeWrapper
 
     /**
      * Callback to be executed whenever the [LayoutNode] is attached to a new [Owner].
      */
-    var onAttach: ((Owner) -> Unit)? = null
+    internal var onAttach: ((Owner) -> Unit)? = null
 
     /**
      * Callback to be executed whenever the [LayoutNode] is detached from an [Owner].
      */
-    var onDetach: ((Owner) -> Unit)? = null
+    internal var onDetach: ((Owner) -> Unit)? = null
 
     /**
      * List of all OnPositioned callbacks in the modifier chain.
@@ -815,7 +777,7 @@
      */
     internal var needsOnPositionedDispatch = false
 
-    fun place(x: Int, y: Int) {
+    internal fun place(x: Int, y: Int) {
         Placeable.PlacementScope.executeWithRtlMirroringValues(
             outerMeasurablePlaceable.measuredWidth,
             layoutDirection
@@ -831,7 +793,7 @@
         outerMeasurablePlaceable.replace()
     }
 
-    fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
+    internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
 
     /**
      * Carries out a hit test on the [PointerInputModifier]s associated with this [LayoutNode] and
@@ -846,7 +808,7 @@
      * @param hitPointerInputFilters The collection that the hit [PointerInputFilter]s will be
      * added to if hit.
      */
-    fun hitTest(
+    internal fun hitTest(
         pointerPositionRelativeToScreen: Offset,
         hitPointerInputFilters: MutableList<PointerInputFilter>
     ) {
@@ -857,7 +819,7 @@
      * Returns the alignment line value for a given alignment line without affecting whether
      * the flag for whether the alignment line was read.
      */
-    fun getAlignmentLine(line: AlignmentLine): Int? {
+    internal fun getAlignmentLine(line: AlignmentLine): Int? {
         val linePos = alignmentLines[line] ?: return null
         var pos = Offset(linePos.toFloat(), linePos.toFloat())
         var wrapper = innerLayoutNodeWrapper
@@ -884,29 +846,11 @@
     }
 
     /**
-     * Find the current inner layer.
-     */
-    private fun updateInnerLayerWrapper() {
-        var delegate: LayoutNodeWrapper? = innerLayoutNodeWrapper
-        val final = outerLayoutNodeWrapper.wrappedBy
-        _innerLayerWrapper = null
-        while (delegate != final) {
-            if (delegate?.layer != null) {
-                _innerLayerWrapper = delegate
-                break
-            }
-            delegate = delegate?.wrappedBy
-        }
-    }
-
-    /**
      * Invoked when the parent placed the node. It will trigger the layout.
      */
     internal fun onNodePlaced() {
         val parent = parent
 
-        updateInnerLayerWrapper()
-
         var newZIndex = innerLayoutNodeWrapper.zIndex
         forEachDelegate {
             newZIndex += it.zIndex
@@ -957,7 +901,7 @@
         // as a result of the previous operation we can figure out a child has been resized
         // and we need to be remeasured, not relaid out
         if (layoutState == NeedsRelayout) {
-            layoutState = LayoutState.LayingOut
+            layoutState = LayingOut
             val owner = requireOwner()
             owner.snapshotObserver.observeLayoutSnapshotReads(this) {
                 // reset the place order counter which will be used by the children
@@ -1074,7 +1018,7 @@
         val parent = parent
         if (parent != null) {
             if (alignmentUsageByParent == UsageByParent.InMeasureBlock &&
-                parent.layoutState != LayoutState.LayingOut
+                parent.layoutState != LayingOut
             ) {
                 parent.requestRemeasure()
             } else if (alignmentUsageByParent == UsageByParent.InLayoutBlock) {
@@ -1090,7 +1034,7 @@
         alignmentLinesQueriedSinceLastLayout = true
         val newUsageByParent = when (parent?.layoutState) {
             Measuring -> UsageByParent.InMeasureBlock
-            LayoutState.LayingOut -> UsageByParent.InLayoutBlock
+            LayingOut -> UsageByParent.InLayoutBlock
             else -> UsageByParent.NotUsed
         }
         val newUsageHasLowerPriority = newUsageByParent == UsageByParent.InLayoutBlock &&
@@ -1137,14 +1081,14 @@
     /**
      * Used to request a new measurement + layout pass from the owner.
      */
-    fun requestRemeasure() {
+    internal fun requestRemeasure() {
         owner?.onRequestMeasure(this)
     }
 
     /**
      * Used to request a new layout pass from the owner.
      */
-    fun requestRelayout() {
+    internal fun requestRelayout() {
         owner?.onRequestRelayout(this)
     }
 
@@ -1152,7 +1096,7 @@
      * Execute your code within the [block] if you want some code to not be observed for the
      * model reads even if you are currently inside some observed scope like measuring.
      */
-    fun ignoreModelReads(block: () -> Unit) {
+    internal fun ignoreModelReads(block: () -> Unit) {
         requireOwner().snapshotObserver.pauseSnapshotReadObservation(block)
     }
 
@@ -1171,7 +1115,7 @@
      * that may be useful. This is used for tooling to retrieve layout modifier and layer
      * information.
      */
-    fun getModifierInfo(): List<ModifierInfo> {
+    override fun getModifierInfo(): List<ModifierInfo> {
         val infoList = mutableVectorOf<ModifierInfo>()
         forEachDelegate { wrapper ->
             wrapper as DelegatingLayoutNodeWrapper<*>
@@ -1204,8 +1148,16 @@
         if (wrapperCache.isEmpty()) {
             return null
         }
-        val index = wrapperCache.indexOfLast {
-            it.modifier === modifier || it.modifier.nativeClass() == modifier.nativeClass()
+        // Look for exact match
+        var index = wrapperCache.indexOfLast {
+            it.toBeReusedForSameModifier && it.modifier === modifier
+        }
+
+        if (index < 0) {
+            // Look for class match
+            index = wrapperCache.indexOfLast {
+                !it.toBeReusedForSameModifier && it.modifier.nativeClass() == modifier.nativeClass()
+            }
         }
 
         if (index < 0) {
@@ -1240,6 +1192,17 @@
         }
     }
 
+    private fun markReusedModifiers(modifier: Modifier) {
+        wrapperCache.forEach {
+            it.toBeReusedForSameModifier = false
+        }
+
+        modifier.foldIn(Unit) { _, mod ->
+            val wrapper = wrapperCache.firstOrNull { it.modifier === mod }
+            wrapper?.toBeReusedForSameModifier = true
+        }
+    }
+
     // Delegation from Measurable to measurableAndPlaceable
     override fun measure(constraints: Constraints) =
         outerMeasurablePlaceable.measure(constraints)
@@ -1295,17 +1258,14 @@
     }
 
     private fun shouldInvalidateParentLayer(): Boolean {
-        if (innerLayerWrapper == null) {
-            return true
-        }
         forEachDelegateIncludingInner {
-            if (it is ModifiedDrawNode) {
-                return true
-            } else if (it.layer != null) {
+            if (it.layer != null) {
                 return false
+            } else if (it is ModifiedDrawNode) {
+                return true
             }
         }
-        error("innerLayerWrapper should have been reached.")
+        return true
     }
 
     /**
@@ -1320,6 +1280,9 @@
         }
     }
 
+    override val parentInfo: LayoutInfo?
+        get() = parent
+
     internal companion object {
         private val ErrorMeasureBlocks: NoIntrinsicsMeasureBlocks =
             object : NoIntrinsicsMeasureBlocks(
@@ -1375,39 +1338,20 @@
 /**
  * Object of pre-allocated lambdas used to make emits to LayoutNodes allocation-less.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 @PublishedApi
 internal object LayoutEmitHelper {
     val constructor: () -> LayoutNode = { LayoutNode() }
     val setModifier: LayoutNode.(Modifier) -> Unit = { this.modifier = it }
     val setDensity: LayoutNode.(Density) -> Unit = { this.density = it }
-    val setMeasureBlocks: LayoutNode.(LayoutNode.MeasureBlocks) -> Unit =
+    val setMeasureBlocks: LayoutNode.(MeasureBlocks) -> Unit =
         { this.measureBlocks = it }
     val setRef: LayoutNode.(Ref<LayoutNode>) -> Unit = { it.value = this }
     val setLayoutDirection: LayoutNode.(LayoutDirection) -> Unit = { this.layoutDirection = it }
 }
 
 /**
- * Returns true if this [LayoutNode] currently has an [LayoutNode.owner].  Semantically,
- * this means that the LayoutNode is currently a part of a component tree.
- */
-@Suppress("NOTHING_TO_INLINE")
-@OptIn(ExperimentalLayoutNodeApi::class)
-internal inline fun LayoutNode.isAttached() = owner != null
-
-/**
- * Used by tooling to examine the modifiers on a [LayoutNode].
- */
-class ModifierInfo(
-    val modifier: Modifier,
-    val coordinates: LayoutCoordinates,
-    val extra: Any? = null
-)
-
-/**
  * Returns [LayoutNode.owner] or throws if it is null.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal fun LayoutNode.requireOwner(): Owner {
     val owner = owner
     checkNotNull(owner) {
@@ -1417,73 +1361,15 @@
 }
 
 /**
- * Inserts a child [LayoutNode] at a last index. If this LayoutNode [isAttached]
- * then [child] will become [isAttached]ed also. [child] must have a `null` [LayoutNode.parent].
+ * Inserts a child [LayoutNode] at a last index. If this LayoutNode [LayoutNode.isAttached]
+ * then [child] will become [LayoutNode.isAttached] also. [child] must have a `null`
+ * [LayoutNode.parent].
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal fun LayoutNode.add(child: LayoutNode) {
     insertAt(children.size, child)
 }
 
 /**
- * Executes [selector] on every parent of this [LayoutNode] and returns the closest
- * [LayoutNode] to return `true` from [selector] or null if [selector] returns false
- * for all ancestors.
- */
-@OptIn(ExperimentalLayoutNodeApi::class)
-fun LayoutNode.findClosestParentNode(selector: (LayoutNode) -> Boolean): LayoutNode? {
-    var currentParent = parent
-    while (currentParent != null) {
-        if (selector(currentParent)) {
-            return currentParent
-        } else {
-            currentParent = currentParent.parent
-        }
-    }
-
-    return null
-}
-
-/**
- * [ContentDrawScope] implementation that extracts density and layout direction information
- * from the given LayoutNodeWrapper
- */
-@OptIn(ExperimentalLayoutNodeApi::class)
-internal class LayoutNodeDrawScope(
-    private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
-) : DrawScope by canvasDrawScope, ContentDrawScope {
-
-    // NOTE, currently a single ComponentDrawScope is shared across composables
-    // which done to allocate a single set of Paint objects and re-use them across
-    // draw calls for all composables.
-    // As a result there could be thread safety concerns here for multi-threaded drawing
-    // scenarios, generally a single ComponentDrawScope should be shared for a particular thread
-    private var wrapped: LayoutNodeWrapper? = null
-
-    override fun drawContent() {
-        drawIntoCanvas { canvas -> wrapped?.draw(canvas) }
-    }
-
-    internal inline fun draw(
-        canvas: Canvas,
-        size: Size,
-        layoutNodeWrapper: LayoutNodeWrapper,
-        block: DrawScope.() -> Unit
-    ) {
-        val previousWrapper = wrapped
-        wrapped = layoutNodeWrapper
-        canvasDrawScope.draw(
-            layoutNodeWrapper.measureScope,
-            layoutNodeWrapper.measureScope.layoutDirection,
-            canvas,
-            size,
-            block
-        )
-        wrapped = previousWrapper
-    }
-}
-
-/**
  * Sets [DelegatingLayoutNodeWrapper#isChained] to `true` of the [wrapped][this.wrapped] when it
  * is part of a chain of LayoutNodes for the same modifier.
  *
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
new file mode 100644
index 0000000..8493379
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+
+/**
+ * [ContentDrawScope] implementation that extracts density and layout direction information
+ * from the given LayoutNodeWrapper
+ */
+internal class LayoutNodeDrawScope(
+    private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
+) : DrawScope by canvasDrawScope, ContentDrawScope {
+
+    // NOTE, currently a single ComponentDrawScope is shared across composables
+    // which done to allocate a single set of Paint objects and re-use them across
+    // draw calls for all composables.
+    // As a result there could be thread safety concerns here for multi-threaded drawing
+    // scenarios, generally a single ComponentDrawScope should be shared for a particular thread
+    private var wrapped: LayoutNodeWrapper? = null
+
+    override fun drawContent() {
+        drawIntoCanvas { canvas -> wrapped?.draw(canvas) }
+    }
+
+    internal inline fun draw(
+        canvas: Canvas,
+        size: Size,
+        LayoutNodeWrapper: LayoutNodeWrapper,
+        block: DrawScope.() -> Unit
+    ) {
+        val previousWrapper = wrapped
+        wrapped = LayoutNodeWrapper
+        canvasDrawScope.draw(
+            LayoutNodeWrapper.measureScope,
+            LayoutNodeWrapper.measureScope.layoutDirection,
+            canvas,
+            size,
+            block
+        )
+        wrapped = previousWrapper
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index de4f6a4..4c350c8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -18,12 +18,12 @@
 
 package androidx.compose.ui.node
 
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.geometry.MutableRect
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDelegatingWrapper
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.graphics.Matrix
@@ -47,7 +47,6 @@
 /**
  * Measurable and Placeable type that has a position.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal abstract class LayoutNodeWrapper(
     internal val layoutNode: LayoutNode
 ) : Placeable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit {
@@ -66,13 +65,14 @@
 
     private var isClipping: Boolean = false
 
-    private var layerBlock: (GraphicsLayerScope.() -> Unit)? = null
+    protected var layerBlock: (GraphicsLayerScope.() -> Unit)? = null
+        private set
 
     private var _isAttached = false
     override val isAttached: Boolean
         get() {
             if (_isAttached) {
-                require(layoutNode.isAttached())
+                require(layoutNode.isAttached)
             }
             return _isAttached
         }
@@ -110,9 +110,10 @@
     var isShallowPlacing = false
 
     private var _rectCache: MutableRect? = null
-    private val rectCache: MutableRect get() = _rectCache ?: MutableRect(0f, 0f, 0f, 0f).also {
-        _rectCache = it
-    }
+    private val rectCache: MutableRect
+        get() = _rectCache ?: MutableRect(0f, 0f, 0f, 0f).also {
+            _rectCache = it
+        }
 
     private val snapshotObserver get() = layoutNode.requireOwner().snapshotObserver
 
@@ -159,9 +160,7 @@
         zIndex: Float,
         layerBlock: (GraphicsLayerScope.() -> Unit)?
     ) {
-        if (wrappedBy?.isShallowPlacing != true) {
-            onLayerBlockUpdated(layerBlock)
-        }
+        onLayerBlockUpdated(layerBlock)
         if (this.position != position) {
             this.position = position
             val layer = layer
@@ -193,7 +192,6 @@
     protected abstract fun performDraw(canvas: Canvas)
 
     // implementation of draw block passed to the OwnedLayer
-    @ExperimentalLayoutNodeApi
     override fun invoke(canvas: Canvas) {
         if (layoutNode.isPlaced) {
             require(layoutNode.layoutState == LayoutNode.LayoutState.Ready) {
@@ -224,6 +222,7 @@
                     move(position)
                 }
                 updateLayerParameters()
+                layoutNode.innerLayerWrapperIsDirty = true
                 invalidateParentLayer()
             } else if (blockHasBeenChanged) {
                 updateLayerParameters()
@@ -231,7 +230,7 @@
         } else {
             layer?.let {
                 it.destroy()
-
+                layoutNode.innerLayerWrapperIsDirty = true
                 invalidateParentLayer()
             }
             layer = null
@@ -414,7 +413,6 @@
         // which layer contained this one, but all layers in this modifier chain will be invalidated
         // in onModifierChanged(). Therefore the only possible layer that won't automatically be
         // invalidated is the parent's layer. We'll invalidate it here:
-        @OptIn(ExperimentalLayoutNodeApi::class)
         layoutNode.parent?.invalidateLayer()
     }
 
@@ -502,14 +500,40 @@
     }
 
     /**
+     * Returns the first [NestedScrollDelegatingWrapper] in the wrapper list that wraps this
+     * [LayoutNodeWrapper].
+     *
+     * Note: This method tried to find [NestedScrollDelegatingWrapper] in the
+     * modifiers before the one wrapped with this [LayoutNodeWrapper] and goes up the hierarchy of
+     * [LayoutNode]s if needed.
+     */
+    abstract fun findPreviousNestedScrollWrapper(): NestedScrollDelegatingWrapper?
+
+    /**
+     * Returns the first [NestedScrollDelegatingWrapper] in the wrapper list that is wrapped by this
+     * [LayoutNodeWrapper].
+     *
+     * Note: This method only goes to the modifiers that follow the one wrapped by
+     * this [LayoutNodeWrapper], it doesn't to the children [LayoutNode]s.
+     */
+    abstract fun findNextNestedScrollWrapper(): NestedScrollDelegatingWrapper?
+
+    /**
      * Returns the first [focus node][ModifiedFocusNode] in the wrapper list that wraps this
      * [LayoutNodeWrapper].
+     *
+     * Note: This method tried to find [NestedScrollDelegatingWrapper] in the
+     * modifiers before the one wrapped with this [LayoutNodeWrapper] and goes up the hierarchy of
+     * [LayoutNode]s if needed.
      */
     abstract fun findPreviousFocusWrapper(): ModifiedFocusNode?
 
     /**
      * Returns the next [focus node][ModifiedFocusNode] in the wrapper list that is wrapped by
      * this [LayoutNodeWrapper].
+     *
+     * Note: This method only goes to the modifiers that follow the one wrapped by
+     * this [LayoutNodeWrapper], it doesn't to the children [LayoutNode]s.
      */
     abstract fun findNextFocusWrapper(): ModifiedFocusNode?
 
@@ -524,8 +548,9 @@
      * that wraps it. The focus state change must be propagated to the parents until we reach
      * another [focus node][ModifiedFocusNode].
      */
-    @OptIn(ExperimentalFocus::class)
-    abstract fun propagateFocusStateChange(focusState: FocusState)
+    open fun propagateFocusEvent(focusState: FocusState) {
+        wrappedBy?.propagateFocusEvent(focusState)
+    }
 
     /**
      * Find the first ancestor that is a [ModifiedFocusNode].
@@ -576,12 +601,19 @@
     /**
      * Returns the first [ModifiedKeyInputNode] in the wrapper list that wraps this
      * [LayoutNodeWrapper].
+     *
+     * Note: This method tried to find [NestedScrollDelegatingWrapper] in the
+     * modifiers before the one wrapped with this [LayoutNodeWrapper] and goes up the hierarchy of
+     * [LayoutNode]s if needed.
      */
     abstract fun findPreviousKeyInputWrapper(): ModifiedKeyInputNode?
 
     /**
      * Returns the next [ModifiedKeyInputNode] in the wrapper list that is wrapped by this
      * [LayoutNodeWrapper].
+     *
+     * Note: This method only goes to the modifiers that follow the one wrapped by
+     * this [LayoutNodeWrapper], it doesn't to the children [LayoutNode]s.
      */
     abstract fun findNextKeyInputWrapper(): ModifiedKeyInputNode?
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
index d012797..289268a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
@@ -23,7 +23,6 @@
  * which is hard to enforce but important to maintain. This method is intended to do the
  * work only during our tests and will iterate through the tree to validate the states consistency.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class LayoutTreeConsistencyChecker(
     private val root: LayoutNode,
     private val relayoutNodes: DepthSortedSet,
@@ -59,7 +58,7 @@
                 return true
             }
             // remeasure or relayout is scheduled
-            val parentLayoutState = parent?.layoutState
+            val parentLayoutState = this.parent?.layoutState
             if (layoutState == LayoutNode.LayoutState.NeedsRemeasure) {
                 return relayoutNodes.contains(this) ||
                     parentLayoutState == LayoutNode.LayoutState.NeedsRemeasure ||
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 489e4e7..8024fb6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
@@ -36,7 +36,6 @@
  * dispatch [OnPositionedModifier] callbacks for the nodes affected by the previous
  * [measureAndLayout] execution.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class MeasureAndLayoutDelegate(private val root: LayoutNode) {
     /**
      * LayoutNodes that need measure or layout.
@@ -190,7 +189,7 @@
      * Iterates through all LayoutNodes that have requested layout and measures and lays them out
      */
     fun measureAndLayout(): Boolean {
-        require(root.isAttached())
+        require(root.isAttached)
         require(root.isPlaced)
         require(!duringMeasureLayout)
         // we don't need to measure any children unless we have the correct root constraints
@@ -226,7 +225,7 @@
                     // execute postponed `onRequestMeasure`
                     if (postponedMeasureRequests.isNotEmpty()) {
                         postponedMeasureRequests.fastForEach {
-                            if (it.isAttached()) {
+                            if (it.isAttached) {
                                 requestRemeasure(it)
                             }
                         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureBlocks.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureBlocks.kt
new file mode 100644
index 0000000..d70ab9b
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureBlocks.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.unit.Constraints
+
+interface MeasureBlocks {
+    /**
+     * The function used to measure the child. It must call [MeasureScope.layout] before
+     * completing.
+     */
+    fun measure(
+        measureScope: MeasureScope,
+        measurables: List<Measurable>,
+        constraints: Constraints
+    ): MeasureResult
+
+    /**
+     * The function used to calculate [IntrinsicMeasurable.minIntrinsicWidth].
+     */
+    fun minIntrinsicWidth(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurables: List<IntrinsicMeasurable>,
+        h: Int
+    ): Int
+
+    /**
+     * The lambda used to calculate [IntrinsicMeasurable.minIntrinsicHeight].
+     */
+    fun minIntrinsicHeight(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurables: List<IntrinsicMeasurable>,
+        w: Int
+    ): Int
+
+    /**
+     * The function used to calculate [IntrinsicMeasurable.maxIntrinsicWidth].
+     */
+    fun maxIntrinsicWidth(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurables: List<IntrinsicMeasurable>,
+        h: Int
+    ): Int
+
+    /**
+     * The lambda used to calculate [IntrinsicMeasurable.maxIntrinsicHeight].
+     */
+    fun maxIntrinsicHeight(
+        intrinsicMeasureScope: IntrinsicMeasureScope,
+        measurables: List<IntrinsicMeasurable>,
+        w: Int
+    ): Int
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
index ec4356a..94ac6c4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
@@ -23,7 +23,6 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.unit.toSize
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class ModifiedDrawNode(
     wrapped: LayoutNodeWrapper,
     drawModifier: DrawModifier
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusObserverNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusEventNode.kt
similarity index 72%
rename from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusObserverNode.kt
rename to compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusEventNode.kt
index 2aa55e8..131b6ff 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusObserverNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusEventNode.kt
@@ -16,21 +16,19 @@
 
 package androidx.compose.ui.node
 
-import androidx.compose.ui.FocusObserverModifier
-import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.focus.FocusEventModifier
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.FocusState.Inactive
 import androidx.compose.ui.focus.searchChildrenForFocusNode
 
-@OptIn(ExperimentalFocus::class)
-internal class ModifiedFocusObserverNode(
+internal class ModifiedFocusEventNode(
     wrapped: LayoutNodeWrapper,
-    modifier: FocusObserverModifier
-) : DelegatingLayoutNodeWrapper<FocusObserverModifier>(wrapped, modifier) {
+    modifier: FocusEventModifier
+) : DelegatingLayoutNodeWrapper<FocusEventModifier>(wrapped, modifier) {
 
-    override fun propagateFocusStateChange(focusState: FocusState) {
-        modifier.onFocusChange(focusState)
-        super.propagateFocusStateChange(focusState)
+    override fun propagateFocusEvent(focusState: FocusState) {
+        modifier.onFocusEvent(focusState)
+        super.propagateFocusEvent(focusState)
     }
 
     override fun onModifierChanged() {
@@ -40,6 +38,6 @@
         // modifier following this observer, it's focus state will be invalid. To solve this, we
         // always reset the focus state when a focus observer is re-used.
         val focusNode = wrapped.findNextFocusWrapper() ?: layoutNode.searchChildrenForFocusNode()
-        modifier.onFocusChange(focusNode?.modifier?.focusState ?: Inactive)
+        modifier.onFocusEvent(focusNode?.modifier?.focusState ?: Inactive)
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
index 2a4055d..9fa1df8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
@@ -17,7 +17,6 @@
 package androidx.compose.ui.node
 
 import androidx.compose.ui.FocusModifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.ActiveParent
@@ -28,10 +27,6 @@
 import androidx.compose.ui.focus.searchChildrenForFocusNode
 import androidx.compose.ui.util.fastForEach
 
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalLayoutNodeApi::class
-)
 internal class ModifiedFocusNode(
     wrapped: LayoutNodeWrapper,
     modifier: FocusModifier
@@ -52,7 +47,7 @@
      */
     fun requestFocus(propagateFocus: Boolean = true) {
         when (modifier.focusState) {
-            Active, Captured, Disabled -> wrappedBy?.propagateFocusStateChange(modifier.focusState)
+            Active, Captured, Disabled -> wrappedBy?.propagateFocusEvent(modifier.focusState)
             ActiveParent -> {
                 val focusedChild = modifier.focusedChild
                 requireNotNull(focusedChild)
@@ -266,12 +261,12 @@
 
     override fun onModifierChanged() {
         super.onModifierChanged()
-        wrappedBy?.propagateFocusStateChange(modifier.focusState)
+        wrappedBy?.propagateFocusEvent(modifier.focusState)
     }
 
     override fun attach() {
         super.attach()
-        wrappedBy?.propagateFocusStateChange(modifier.focusState)
+        wrappedBy?.propagateFocusEvent(modifier.focusState)
     }
 
     override fun detach() {
@@ -287,9 +282,9 @@
                     ?: layoutNode.searchChildrenForFocusNode()
                 if (nextFocusNode != null) {
                     findParentFocusNode()?.modifier?.focusedChild = nextFocusNode
-                    wrappedBy?.propagateFocusStateChange(nextFocusNode.modifier.focusState)
+                    wrappedBy?.propagateFocusEvent(nextFocusNode.modifier.focusState)
                 } else {
-                    wrappedBy?.propagateFocusStateChange(Inactive)
+                    wrappedBy?.propagateFocusEvent(Inactive)
                 }
             }
             // TODO(b/155212782): Implement this after adding support for disabling focus modifiers.
@@ -305,7 +300,7 @@
 
     override fun findNextFocusWrapper() = this
 
-    override fun propagateFocusStateChange(focusState: FocusState) {
+    override fun propagateFocusEvent(focusState: FocusState) {
         // Do nothing. Stop propagating the focus change (since we hit another focus node).
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
index c59af87..569edcc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
@@ -17,13 +17,9 @@
 package androidx.compose.ui.node
 
 import androidx.compose.ui.FocusRequesterModifier
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.searchChildrenForFocusNode
 
-@OptIn(
-    ExperimentalFocus::class
-)
 internal class ModifiedFocusRequesterNode(
     wrapped: LayoutNodeWrapper,
     modifier: FocusRequesterModifier
@@ -38,7 +34,6 @@
         }
 
     // Searches for the focus node associated with this focus requester node.
-    @OptIn(ExperimentalLayoutNodeApi::class)
     internal fun findFocusNode(): ModifiedFocusNode? {
         return findNextFocusWrapper() ?: layoutNode.searchChildrenForFocusNode()
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt
index 1c4856a..17b728f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt
@@ -16,11 +16,9 @@
 
 package androidx.compose.ui.node
 
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyInputModifier
 
-@OptIn(ExperimentalKeyInput::class)
 internal class ModifiedKeyInputNode(wrapped: LayoutNodeWrapper, modifier: KeyInputModifier) :
     DelegatingLayoutNodeWrapper<KeyInputModifier>(wrapped, modifier) {
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
index 0904cee..8e952e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class ModifiedLayoutNode(
     wrapped: LayoutNodeWrapper,
     modifier: LayoutModifier
@@ -67,7 +66,7 @@
         }
         // Place our wrapped to obtain their position inside ourselves.
         isShallowPlacing = true
-        placeAt(this.position, zIndex = 0f, null)
+        placeAt(this.position, this.zIndex, this.layerBlock)
         isShallowPlacing = false
         return if (line is HorizontalAlignmentLine) {
             positionInWrapped + wrapped.position.y
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedParentDataNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedParentDataNode.kt
index adcfe33..521c2c0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedParentDataNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedParentDataNode.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.ui.layout.ParentDataModifier
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class ModifiedParentDataNode(
     wrapped: LayoutNodeWrapper,
     parentDataModifier: ParentDataModifier
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt
index ebbc6c0..867cd1c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt
@@ -23,7 +23,6 @@
  * Tracks the nodes being positioned and dispatches OnPositioned callbacks when we finished
  * the measure/layout pass.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class OnPositionedDispatcher {
     private val layoutNodes = mutableVectorOf<LayoutNode>()
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
index d8b010e..243173d9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
@@ -25,7 +25,6 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class OuterMeasurablePlaceable(
     private val layoutNode: LayoutNode,
     var outerWrapper: LayoutNodeWrapper
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index f5107fe..97e3311 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -15,13 +15,12 @@
  */
 package androidx.compose.ui.node
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.autofill.Autofill
 import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
@@ -39,7 +38,6 @@
  * to Android [views][android.view.View] and all layout, draw, input, and accessibility is hooked
  * through them.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 interface Owner {
 
     /**
@@ -68,11 +66,13 @@
      *  TODO(ralu): Replace with SemanticsTree. This is a temporary hack until we have a semantics
      *  tree implemented.
      */
+    @ExperimentalComposeUiApi
     val autofillTree: AutofillTree
 
     /**
      * The [Autofill] class can be used to perform autofill operations. It is used as an ambient.
      */
+    @ExperimentalComposeUiApi
     val autofill: Autofill?
 
     val density: Density
@@ -84,7 +84,6 @@
     /**
      * Provide a focus manager that controls focus within Compose.
      */
-    @ExperimentalFocus
     val focusManager: FocusManager
 
     /**
@@ -115,11 +114,6 @@
     fun onRequestRelayout(layoutNode: LayoutNode)
 
     /**
-     * Whether the Owner has pending layout work.
-     */
-    val hasPendingMeasureOrLayout: Boolean
-
-    /**
      * Called by [LayoutNode] when it is attached to the view system and now has an owner.
      * This is used by [Owner] to track which nodes are associated with it. It will only be
      * called when [node] is not already attached to an owner.
@@ -151,7 +145,6 @@
      *
      * @return true if the event was consumed. False otherwise.
      */
-    @ExperimentalKeyInput
     fun sendKeyEvent(keyEvent: KeyEvent): Boolean
 
     /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
index 663dde03..b7bec7f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
@@ -24,7 +24,7 @@
  * automatically when the snapshot value has been changed.
  */
 // TODO make it internal once Owner is internal
-@OptIn(ExperimentalComposeApi::class, ExperimentalLayoutNodeApi::class)
+@OptIn(ExperimentalComposeApi::class)
 @Suppress("CallbackName") // TODO rename this and SnapshotStateObserver. b/173401548
 class OwnerSnapshotObserver(onChangedExecutor: (callback: () -> Unit) -> Unit) {
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
index bfbb003..c4f6b6d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
@@ -20,9 +20,9 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.staticAmbientOf
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.autofill.Autofill
 import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.node.Owner
@@ -52,19 +52,7 @@
 /**
  * The ambient that can be used to trigger autofill actions. Eg. [Autofill.requestAutofillForNode].
  */
-@Suppress("AmbientNaming")
-@Deprecated(
-    "Renamed to AmbientAutofill",
-    replaceWith = ReplaceWith(
-        "AmbientAutofill",
-        "androidx.compose.ui.platform.AmbientAutofill"
-    )
-)
-val AutofillAmbient get() = AmbientAutofill
-
-/**
- * The ambient that can be used to trigger autofill actions. Eg. [Autofill.requestAutofillForNode].
- */
+@ExperimentalComposeUiApi
 val AmbientAutofill = staticAmbientOf<Autofill?>()
 
 /**
@@ -73,22 +61,7 @@
  * [AutofillTree] is a temporary data structure that will be replaced by Autofill Semantics
  * (b/138604305).
  */
-@Suppress("AmbientNaming")
-@Deprecated(
-    "Renamed to AmbientAutofillTree",
-    replaceWith = ReplaceWith(
-        "AmbientAutofillTree",
-        "androidx.compose.ui.platform.AmbientAutofillTree"
-    )
-)
-val AutofillTreeAmbient get() = AmbientAutofillTree
-
-/**
- * The ambient that can be used to add
- * [AutofillNode][import androidx.compose.ui.autofill.AutofillNode]s to the autofill tree. The
- * [AutofillTree] is a temporary data structure that will be replaced by Autofill Semantics
- * (b/138604305).
- */
+@ExperimentalComposeUiApi
 val AmbientAutofillTree = staticAmbientOf<AutofillTree>()
 
 /**
@@ -146,13 +119,11 @@
         "androidx.compose.ui.platform.AmbientFocusManager"
     )
 )
-@ExperimentalFocus
 val FocusManagerAmbient get() = AmbientFocusManager
 
 /**
  * The ambient that can be used to control focus within Compose.
  */
-@ExperimentalFocus
 val AmbientFocusManager = staticAmbientOf<FocusManager>()
 
 /**
@@ -292,7 +263,7 @@
  */
 val AmbientWindowManager = staticAmbientOf<WindowManager>()
 
-@OptIn(ExperimentalFocus::class)
+@ExperimentalComposeUiApi
 @Composable
 internal fun ProvideCommonAmbients(
     owner: Owner,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt
index d821c90..7e4506e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt
@@ -18,22 +18,12 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composition
 import androidx.compose.runtime.CompositionReference
-import androidx.compose.runtime.ExperimentalComposeApi
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.util.annotation.MainThread
 
-@OptIn(ExperimentalLayoutNodeApi::class)
-internal expect fun actualSubcomposeInto(
-    container: LayoutNode,
-    parent: CompositionReference,
-    composable: @Composable () -> Unit
-): Composition
-
-@OptIn(ExperimentalComposeApi::class, ExperimentalLayoutNodeApi::class)
 @MainThread
-fun subcomposeInto(
+internal expect fun subcomposeInto(
     container: LayoutNode,
     parent: CompositionReference,
     composable: @Composable () -> Unit
-): Composition = actualSubcomposeInto(container, parent, composable)
\ No newline at end of file
+): Composition
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
index 26d168e..b98c168 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
@@ -20,11 +20,13 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 
 /**
  * Provides [Selection] information for a composable to SelectionContainer. Composables who can
  * be selected should subscribe to [SelectionRegistrar] using this interface.
  */
+@ExperimentalTextApi
 interface Selectable {
     /**
      * Returns [Selection] information for a selectable composable. If no selection can be provided
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
index 714ac17..bb80ac9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.selection
 
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.style.ResolvedTextDirection
 
@@ -24,6 +25,7 @@
  * Information about the current Selection.
  */
 @Immutable
+@OptIn(ExperimentalTextApi::class)
 data class Selection(
     /**
      * Information about the start of the selection.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
index 3e4b606..be22667 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
@@ -135,17 +135,12 @@
                         handle = null
                     )
                 }
-                SelectionFloatingToolBar(manager = manager)
             }
         }
     }
 
     onDispose {
         manager.selection = null
+        manager.hideSelectionToolbar()
     }
 }
-
-@Composable
-private fun SelectionFloatingToolBar(manager: SelectionManager) {
-    manager.showSelectionToolbar()
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
index b6cbf8e..ce33357 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.TextToolbarStatus
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.length
 import androidx.compose.ui.text.subSequence
@@ -40,7 +41,10 @@
 /**
  * A bridge class between user interaction to the text composables for text selection.
  */
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
     /**
      * The current selection.
@@ -49,7 +53,6 @@
         set(value) {
             field = value
             updateHandleOffsets()
-            hideSelectionToolbar()
         }
 
     /**
@@ -123,9 +126,20 @@
     init {
         selectionRegistrar.onPositionChangeCallback = {
             updateHandleOffsets()
+            updateSelectionToolbarPosition()
+        }
+
+        selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
+            updateSelection(
+                startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
+                endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
+                isStartHandle = true,
+                longPress = true
+            )
             hideSelectionToolbar()
         }
-        selectionRegistrar.onUpdateSelectionCallback =
+
+        selectionRegistrar.onSelectionUpdateCallback =
             { layoutCoordinates, startPosition, endPosition ->
                 updateSelection(
                     startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
@@ -134,6 +148,10 @@
                     longPress = true
                 )
             }
+
+        selectionRegistrar.onSelectionUpdateEndCallback = {
+            showSelectionToolbar()
+        }
     }
 
     private fun updateHandleOffsets() {
@@ -265,12 +283,9 @@
         }
     }
 
-    private fun hideSelectionToolbar() {
+    internal fun hideSelectionToolbar() {
         if (textToolbar?.status == TextToolbarStatus.Shown) {
-            val selection = selection
-            if (selection == null) {
-                textToolbar?.hide()
-            }
+            textToolbar?.hide()
         }
     }
 
@@ -356,12 +371,14 @@
             endPosition = Offset(-1f, -1f),
             previousSelection = selection
         )
+        hideSelectionToolbar()
         if (selection != null) onSelectionChange(null)
     }
 
     fun handleDragObserver(isStartHandle: Boolean): DragObserver {
         return object : DragObserver {
             override fun onStart(downPosition: Offset) {
+                hideSelectionToolbar()
                 val selection = selection!!
                 // The LayoutCoordinates of the composable where the drag gesture should begin. This
                 // is used to convert the position of the beginning of the drag gesture from the
@@ -375,13 +392,15 @@
                 // The position of the character where the drag gesture should begin. This is in
                 // the composable coordinates.
                 val beginCoordinates = getAdjustedCoordinates(
-                    if (isStartHandle)
+                    if (isStartHandle) {
                         selection.start.selectable.getHandlePosition(
                             selection = selection, isStartHandle = true
-                        ) else
+                        )
+                    } else {
                         selection.end.selectable.getHandlePosition(
                             selection = selection, isStartHandle = false
                         )
+                    }
                 )
 
                 // Convert the position where drag gesture begins from composable coordinates to
@@ -434,6 +453,14 @@
                 )
                 return dragDistance
             }
+
+            override fun onStop(velocity: Offset) {
+                showSelectionToolbar()
+            }
+
+            override fun onCancel() {
+                showSelectionToolbar()
+            }
         }
     }
 
@@ -468,6 +495,7 @@
     return lhs?.merge(rhs) ?: rhs
 }
 
+@OptIn(ExperimentalTextApi::class)
 internal fun getCurrentSelectedText(
     selectable: Selectable,
     selection: Selection
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
index 3dbff76..7e1cded 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
@@ -19,10 +19,12 @@
 import androidx.compose.runtime.ambientOf
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 
 /**
  *  An interface allowing a composable to subscribe and unsubscribe to selection changes.
  */
+@ExperimentalTextApi
 interface SelectionRegistrar {
     /**
      * Subscribe to SelectionContainer selection changes.
@@ -38,20 +40,58 @@
      * When the Global Position of a subscribed [Selectable] changes, this method
      * is called.
      */
-    fun onPositionChange()
+    fun notifyPositionChange()
 
     /**
-     * When selection changes, this method is called.
+     * Call this method to notify the [SelectionContainer] that the selection has been initiated.
+     * Depends on the input, [notifySelectionUpdate] may be called repeatedly after
+     * [notifySelectionUpdateStart] is called. And [notifySelectionUpdateEnd] should always be
+     * called after selection finished.
+     * For example:
+     *  1. User long pressed the text and then release. [notifySelectionUpdateStart] should be
+     *  called followed by [notifySelectionUpdateEnd] being called once.
+     *  2. User long pressed the text and then drag a distance and then release.
+     *  [notifySelectionUpdateStart] should be called first after the user long press, and then
+     *  [notifySelectionUpdate] is called several times reporting the updates, in the end
+     *  [notifySelectionUpdateEnd] is called to finish the selection.
+     *
+     * @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
+     * @param startPosition coordinates of where the selection is initiated.
+     *
+     * @see notifySelectionUpdate
+     * @see notifySelectionUpdateEnd
+     */
+    fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset
+    )
+
+    /**
+     * Call this method to notify the [SelectionContainer] that  the selection has been updated.
+     * The caller of this method should make sure that [notifySelectionUpdateStart] is always
+     * called once before calling this function. And [notifySelectionUpdateEnd] is always called
+     * once after the all updates finished.
      *
      * @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
      * @param startPosition coordinates of where the selection starts.
      * @param endPosition coordinates of where the selection ends.
+     *
+     * @see notifySelectionUpdateStart
+     * @see notifySelectionUpdateEnd
      */
-    fun onUpdateSelection(
+    fun notifySelectionUpdate(
         layoutCoordinates: LayoutCoordinates,
         startPosition: Offset,
-        endPosition: Offset
+        endPosition: Offset,
     )
+
+    /**
+     * Call this method to notify the [SelectionContainer] that the selection update has stopped.
+     *
+     * @see notifySelectionUpdateStart
+     * @see notifySelectionUpdate
+     */
+    fun notifySelectionUpdateEnd()
 }
 
 /**
@@ -72,4 +112,5 @@
  * Ambient of SelectionRegistrar. Composables that implement selection logic can use this ambient
  * to get a [SelectionRegistrar] in order to subscribe and unsubscribe to [SelectionRegistrar].
  */
+@OptIn(ExperimentalTextApi::class)
 val AmbientSelectionRegistrar = ambientOf<SelectionRegistrar?>()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
index c42a8d1..dd9250d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
@@ -18,7 +18,9 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 
+@OptIn(ExperimentalTextApi::class)
 internal class SelectionRegistrarImpl : SelectionRegistrar {
     /**
      * A flag to check if the [Selectable]s have already been sorted.
@@ -42,6 +44,21 @@
      */
     internal var onPositionChangeCallback: (() -> Unit)? = null
 
+    /**
+     * The callback to be invoked when the selection is initiated.
+     */
+    internal var onSelectionUpdateStartCallback: ((LayoutCoordinates, Offset) -> Unit)? = null
+
+    /**
+     * The callback to be invoked when the selection is updated.
+     */
+    internal var onSelectionUpdateCallback: ((LayoutCoordinates, Offset, Offset) -> Unit)? = null
+
+    /**
+     * The callback to be invoked when selection update finished.
+     */
+    internal var onSelectionUpdateEndCallback: (() -> Unit)? = null
+
     override fun subscribe(selectable: Selectable): Selectable {
         _selectables.add(selectable)
         sorted = false
@@ -87,27 +104,29 @@
         return selectables
     }
 
-    override fun onPositionChange() {
+    override fun notifyPositionChange() {
         // Set the variable sorted to be false, when the global position of a registered
         // selectable changes.
         sorted = false
         onPositionChangeCallback?.invoke()
     }
 
-    /**
-     * The callback to be invoked when the selection change was triggered.
-     */
-    internal var onUpdateSelectionCallback: ((LayoutCoordinates, Offset, Offset) -> Unit)? = null
+    override fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset
+    ) {
+        onSelectionUpdateStartCallback?.invoke(layoutCoordinates, startPosition)
+    }
 
-    override fun onUpdateSelection(
+    override fun notifySelectionUpdate(
         layoutCoordinates: LayoutCoordinates,
         startPosition: Offset,
         endPosition: Offset
     ) {
-        onUpdateSelectionCallback?.invoke(
-            layoutCoordinates,
-            startPosition,
-            endPosition
-        )
+        onSelectionUpdateCallback?.invoke(layoutCoordinates, startPosition, endPosition)
+    }
+
+    override fun notifySelectionUpdateEnd() {
+        onSelectionUpdateEndCallback?.invoke()
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
index ff04b6a..ead5b9b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
@@ -75,15 +75,7 @@
      * [SemanticsNode] representing the owning component.
      */
     var isMergingSemanticsOfDescendants: Boolean = false
-
-    /**
-     * Whether this configuration is empty.
-     *
-     * An empty configuration doesn't contain any semantic information that it
-     * wants to contribute to the semantics tree.
-     */
-    val isEmpty: Boolean
-        get() = props.isEmpty() && !isMergingSemanticsOfDescendants
+    var isClearingSemantics: Boolean = false
 
     // CONFIGURATION COMBINATION LOGIC
 
@@ -116,6 +108,9 @@
         if (peer.isMergingSemanticsOfDescendants) {
             isMergingSemanticsOfDescendants = true
         }
+        if (peer.isClearingSemantics) {
+            isClearingSemantics = true
+        }
         for ((key, nextValue) in peer.props) {
             if (!props.contains(key)) {
                 props[key] = nextValue
@@ -127,6 +122,7 @@
     fun copy(): SemanticsConfiguration {
         val copy = SemanticsConfiguration()
         copy.isMergingSemanticsOfDescendants = isMergingSemanticsOfDescendants
+        copy.isClearingSemantics = isClearingSemantics
         copy.props.putAll(props)
         return copy
     }
@@ -135,8 +131,9 @@
         if (this === other) return true
         if (other !is SemanticsConfiguration) return false
 
-        if (isMergingSemanticsOfDescendants != other.isMergingSemanticsOfDescendants) return false
         if (props != other.props) return false
+        if (isMergingSemanticsOfDescendants != other.isMergingSemanticsOfDescendants) return false
+        if (isClearingSemantics != other.isClearingSemantics) return false
 
         return true
     }
@@ -144,6 +141,7 @@
     override fun hashCode(): Int {
         var result = props.hashCode()
         result = 31 * result + isMergingSemanticsOfDescendants.hashCode()
+        result = 31 * result + isClearingSemantics.hashCode()
         return result
     }
 
@@ -157,6 +155,12 @@
             nextSeparator = ", "
         }
 
+        if (isClearingSemantics) {
+            propsString.append(nextSeparator)
+            propsString.append("isClearingSemantics=true")
+            nextSeparator = ", "
+        }
+
         for ((key, value) in props) {
             propsString.append(nextSeparator)
             propsString.append(key.name)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
index 50c29ab..0135adf 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
@@ -44,11 +44,13 @@
 internal class SemanticsModifierCore(
     override val id: Int,
     mergeDescendants: Boolean,
+    clearAndSetSemantics: Boolean,
     properties: (SemanticsPropertyReceiver.() -> Unit)
 ) : SemanticsModifier {
     override val semanticsConfiguration: SemanticsConfiguration =
         SemanticsConfiguration().also {
             it.isMergingSemanticsOfDescendants = mergeDescendants
+            it.isClearingSemantics = clearAndSetSemantics
 
             it.properties()
         }
@@ -73,11 +75,32 @@
 }
 
 /**
- * Add semantics key/value for use in testing, accessibility, and similar use cases.
+ * Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
+ *
+ * The provided lambda receiver scope provides "key = value"-style setters for any
+ * [SemanticsPropertyKey]. Additionally, chaining multiple semantics modifiers is
+ * also a supported style.
+ *
+ * The resulting semantics produce two [SemanticsNode] trees:
+ *
+ * The "unmerged tree" rooted at [SemanticsOwner.unmergedRootSemanticsNode] has one
+ * [SemanticsNode] per layout node which has any [SemanticsModifier] on it.  This [SemanticsNode]
+ * contains all the properties set in all the [SemanticsModifier]s on that node.
+ *
+ * The "merged tree" rooted at [SemanticsOwner.rootSemanticsNode] has equal-or-fewer nodes: it
+ * simplifies the structure based on [mergeDescendants] and [clearAndSetSemantics].  For most
+ * purposes (especially accessibility, or the testing of accessibility), the merged semantics
+ * tree should be used.
  *
  * @param mergeDescendants Whether the semantic information provided by the owning component and
- * its descendants (which do not themselves merge descendants) should be treated as one logical
- * entity.
+ * its descendants should be treated as one logical entity.
+ * Most commonly set on screen-reader-focusable items such as buttons or form fields.
+ * In the merged semantics tree, all descendant nodes (except those themselves marked
+ * [mergeDescendants]) will disappear from the tree, and their properties will get merged
+ * into the parent's configuration (using a merging algorithm that varies based on the type
+ * of property -- for example, text properties will get concatenated, separated by commas).
+ * In the unmerged semantics tree, the node is simply marked with
+ * [SemanticsConfiguration.isMergingSemanticsOfDescendants].
  * @param properties properties to add to the semantics. [SemanticsPropertyReceiver] will be
  * provided in the scope to allow access for common properties and its values.
  */
@@ -92,5 +115,32 @@
     }
 ) {
     val id = remember { SemanticsModifierCore.generateSemanticsId() }
-    SemanticsModifierCore(id, mergeDescendants, properties)
+    SemanticsModifierCore(id, mergeDescendants, clearAndSetSemantics = false, properties)
+}
+
+/**
+ * Clears the semantics of all the descendant nodes and sets new semantics.
+ *
+ * In the merged semantics tree, this clears the semantic information provided
+ * by the node's descendants (but not those of the layout node itself, if any) and sets
+ * the provided semantics.  (In the unmerged tree, the semantics node is marked with
+ * "[SemanticsConfiguration.isClearingSemantics]", but nothing is actually cleared.)
+ *
+ * Compose's default semantics provide baseline usability for screen-readers, but this can be
+ * used to provide a more polished screen-reader experience: for example, clearing the
+ * semantics of a group of tiny buttons, and setting equivalent actions on the card containing them.
+ *
+ * @param properties properties to add to the semantics. [SemanticsPropertyReceiver] will be
+ * provided in the scope to allow access for common properties and its values.
+ */
+fun Modifier.clearAndSetSemantics(
+    properties: (SemanticsPropertyReceiver.() -> Unit)
+): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "clearAndSetSemantics"
+        this.properties["properties"] = properties
+    }
+) {
+    val id = remember { SemanticsModifierCore.generateSemanticsId() }
+    SemanticsModifierCore(id, mergeDescendants = false, clearAndSetSemantics = true, properties)
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 4e12cbf..bca6c84 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -23,10 +23,10 @@
 import androidx.compose.ui.layout.globalBounds
 import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
+import androidx.compose.ui.layout.LayoutInfo
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNodeWrapper
-import androidx.compose.ui.node.findClosestParentNode
+import androidx.compose.ui.node.Owner
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
 
@@ -47,7 +47,6 @@
  * of any other semantics modifiers on the same layout node, and if "mergeDescendants" is
  * specified and enabled, also the "merged" configuration of its subtree.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 class SemanticsNode internal constructor(
     /*
      * This is expected to be the outermost semantics modifier on a layout node.
@@ -67,8 +66,22 @@
 ) {
     internal val unmergedConfig = layoutNodeWrapper.collapsedSemanticsConfiguration()
     val id: Int = layoutNodeWrapper.modifier.id
-    // TODO(aelias): Make this internal and expose the Owner instead
-    val componentNode: LayoutNode = layoutNodeWrapper.layoutNode
+
+    /**
+     * The [LayoutInfo] that this is associated with.
+     */
+    val layoutInfo: LayoutInfo = layoutNodeWrapper.layoutNode
+
+    /**
+     * The [Owner] this node is attached to.
+     */
+    // TODO(b/174747742) Stop using Owner in tests and use RootForTest instead
+    val owner: Owner? get() = layoutNode.owner
+
+    /**
+     * The [LayoutNode] that this is associated with.
+     */
+    internal val layoutNode: LayoutNode = layoutNodeWrapper.layoutNode
 
     // GEOMETRY
 
@@ -77,7 +90,7 @@
      */
     val size: IntSize
         get() {
-            return componentNode.coordinates.size
+            return this.layoutNode.coordinates.size
         }
 
     /**
@@ -87,7 +100,7 @@
      */
     val boundsInRoot: Rect
         get() {
-            return componentNode.coordinates.boundsInRoot
+            return this.layoutNode.coordinates.boundsInRoot
         }
 
     /**
@@ -96,7 +109,7 @@
      */
     val positionInRoot: Offset
         get() {
-            return componentNode.coordinates.positionInRoot
+            return this.layoutNode.coordinates.positionInRoot
         }
 
     /**
@@ -105,7 +118,7 @@
      */
     val globalBounds: Rect
         get() {
-            return componentNode.coordinates.globalBounds
+            return this.layoutNode.coordinates.globalBounds
         }
 
     /**
@@ -113,7 +126,7 @@
      */
     val globalPosition: Offset
         get() {
-            return componentNode.coordinates.globalPosition
+            return this.layoutNode.coordinates.globalPosition
         }
 
     /**
@@ -121,7 +134,7 @@
      * if the line is not provided.
      */
     fun getAlignmentLinePosition(line: AlignmentLine): Int {
-        return componentNode.coordinates[line]
+        return this.layoutNode.coordinates[line]
     }
 
     // CHILDREN
@@ -139,9 +152,7 @@
         get() {
             if (isMergingSemanticsOfDescendants) {
                 val mergedConfig = unmergedConfig.copy()
-                unmergedChildren().fastForEach { child ->
-                    child.mergeConfig(mergedConfig)
-                }
+                mergeConfig(mergedConfig)
                 return mergedConfig
             } else {
                 return unmergedConfig
@@ -149,15 +160,17 @@
         }
 
     private fun mergeConfig(mergedConfig: SemanticsConfiguration) {
-        // Don't merge children that themselves merge all their descendants (because that
-        // indicates they're independently screen-reader-focusable).
-        if (isMergingSemanticsOfDescendants) {
-            return
-        }
+        if (!unmergedConfig.isClearingSemantics) {
+            unmergedChildren().fastForEach { child ->
+                // Don't merge children that themselves merge all their descendants (because that
+                // indicates they're independently screen-reader-focusable).
+                if (child.isMergingSemanticsOfDescendants) {
+                    return
+                }
 
-        mergedConfig.mergeChild(unmergedConfig)
-        unmergedChildren().fastForEach { child ->
-            child.mergeConfig(mergedConfig)
+                mergedConfig.mergeChild(child.unmergedConfig)
+                child.mergeConfig(mergedConfig)
+            }
         }
     }
 
@@ -167,7 +180,7 @@
     internal fun unmergedChildren(): List<SemanticsNode> {
         val unmergedChildren: MutableList<SemanticsNode> = mutableListOf()
 
-        val semanticsChildren = componentNode.findOneLayerOfSemanticsWrappers()
+        val semanticsChildren = this.layoutNode.findOneLayerOfSemanticsWrappers()
         semanticsChildren.fastForEach { semanticsChild ->
             unmergedChildren.add(SemanticsNode(semanticsChild, mergingEnabled))
         }
@@ -184,6 +197,11 @@
     //               optimize this when the merging algorithm is improved.
     val children: List<SemanticsNode>
         get() {
+            // Replacing semantics never appear to have any children in the merged tree.
+            if (mergingEnabled && unmergedConfig.isClearingSemantics) {
+                return listOf()
+            }
+
             if (isMergingSemanticsOfDescendants) {
                 // In most common merging scenarios like Buttons, this will return nothing.
                 // In cases like a clickable Row itself containing a Button, this will
@@ -233,7 +251,7 @@
         get() {
             var node: LayoutNode? = null
             if (mergingEnabled) {
-                node = componentNode.findClosestParentNode {
+                node = this.layoutNode.findClosestParentNode {
                     it.outerSemantics
                         ?.collapsedSemanticsConfiguration()
                         ?.isMergingSemanticsOfDescendants == true
@@ -241,7 +259,7 @@
             }
 
             if (node == null) {
-                node = componentNode.findClosestParentNode { it.outerSemantics != null }
+                node = this.layoutNode.findClosestParentNode { it.outerSemantics != null }
             }
 
             val outerSemantics = node?.outerSemantics
@@ -258,7 +276,9 @@
             if (child.isMergingSemanticsOfDescendants == true) {
                 list.add(child)
             } else {
-                child.findOneLayerOfMergingSemanticsNodes(list)
+                if (child.unmergedConfig.isClearingSemantics == false) {
+                    child.findOneLayerOfMergingSemanticsNodes(list)
+                }
             }
         }
         return list
@@ -268,7 +288,6 @@
 /**
  * Returns the outermost semantics node on a LayoutNode.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal val LayoutNode.outerSemantics: SemanticsWrapper?
     get() {
         return outerLayoutNodeWrapper.nearestSemantics
@@ -296,7 +315,6 @@
     return null
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 private fun LayoutNode.findOneLayerOfSemanticsWrappers(
     list: MutableList<SemanticsWrapper> = mutableListOf<SemanticsWrapper>()
 ): List<SemanticsWrapper> {
@@ -310,3 +328,21 @@
     }
     return list
 }
+
+/**
+ * Executes [selector] on every parent of this [LayoutNode] and returns the closest
+ * [LayoutNode] to return `true` from [selector] or null if [selector] returns false
+ * for all ancestors.
+ */
+private fun LayoutNode.findClosestParentNode(selector: (LayoutNode) -> Boolean): LayoutNode? {
+    var currentParent = this.parent
+    while (currentParent != null) {
+        if (selector(currentParent)) {
+            return currentParent
+        } else {
+            currentParent = currentParent.parent
+        }
+    }
+
+    return null
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
index 2eb1ecc..d7e2128 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.semantics
 
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.util.fastForEach
 
@@ -24,8 +23,7 @@
  * Owns [SemanticsNode] objects and notifies listeners of changes to the
  * semantics tree
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
-class SemanticsOwner(val rootNode: LayoutNode) {
+class SemanticsOwner internal constructor(val rootNode: LayoutNode) {
     /**
      * The root node of the semantics tree.  Does not contain any unmerged data.
      * May contain merged data.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index a06831b..ca1996d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -20,7 +20,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.util.annotation.IntRange
 import kotlin.reflect.KProperty
@@ -33,10 +33,10 @@
      * Developer-set content description of the semantics node. If this is not set, accessibility
      * services will present the [Text] of this node as content part.
      *
-     * @see SemanticsPropertyReceiver.accessibilityLabel
+     * @see SemanticsPropertyReceiver.contentDescription
      */
-    val AccessibilityLabel = SemanticsPropertyKey<String>(
-        name = "AccessibilityLabel",
+    val ContentDescription = SemanticsPropertyKey<String>(
+        name = "ContentDescription",
         mergePolicy = { parentValue, childValue ->
             if (parentValue == null) {
                 childValue
@@ -52,14 +52,14 @@
      * [AccessibilityRangeInfo], but it is not guaranteed and the format will be decided by
      * accessibility services.
      *
-     * @see SemanticsPropertyReceiver.accessibilityValue
+     * @see SemanticsPropertyReceiver.stateDescription
      */
-    val AccessibilityValue = SemanticsPropertyKey<String>("AccessibilityValue")
+    val StateDescription = SemanticsPropertyKey<String>("StateDescription")
 
     /**
      * The node is a range with current value.
      *
-     * @see SemanticsPropertyReceiver.accessibilityValueRange
+     * @see SemanticsPropertyReceiver.stateDescriptionRange
      */
     val AccessibilityRangeInfo =
         SemanticsPropertyKey<AccessibilityRangeInfo>("AccessibilityRangeInfo")
@@ -155,7 +155,7 @@
             if (parentValue == null) {
                 childValue
             } else {
-                annotatedString {
+                buildAnnotatedString {
                     append(parentValue)
                     append(", ")
                     append(childValue)
@@ -410,9 +410,15 @@
  * Developer-set content description of the semantics node. If this is not set, accessibility
  * services will present the text of this node as content part.
  *
- * @see SemanticsProperties.AccessibilityLabel
+ * @see SemanticsProperties.ContentDescription
  */
-var SemanticsPropertyReceiver.accessibilityLabel by SemanticsProperties.AccessibilityLabel
+var SemanticsPropertyReceiver.contentDescription by SemanticsProperties.ContentDescription
+
+@Deprecated(
+    "accessibilityLabel was renamed to contentDescription",
+    ReplaceWith("contentDescription", "androidx.compose.ui.semantics")
+)
+var SemanticsPropertyReceiver.accessibilityLabel by SemanticsProperties.ContentDescription
 
 /**
  * Developer-set state description of the semantics node. For example: on/off. If this not
@@ -420,16 +426,22 @@
  * [AccessibilityRangeInfo], but it is not guaranteed and the format will be decided by
  * accessibility services.
  *
- * @see SemanticsProperties.AccessibilityValue
+ * @see SemanticsProperties.StateDescription
  */
-var SemanticsPropertyReceiver.accessibilityValue by SemanticsProperties.AccessibilityValue
+var SemanticsPropertyReceiver.stateDescription by SemanticsProperties.StateDescription
+
+@Deprecated(
+    "accessibilityValue was renamed to stateDescription",
+    ReplaceWith("stateDescription", "androidx.compose.ui.semantics")
+)
+var SemanticsPropertyReceiver.accessibilityValue by SemanticsProperties.StateDescription
 
 /**
  * The node is a range with current value.
  *
  * @see SemanticsProperties.AccessibilityRangeInfo
  */
-var SemanticsPropertyReceiver.accessibilityValueRange by SemanticsProperties.AccessibilityRangeInfo
+var SemanticsPropertyReceiver.stateDescriptionRange by SemanticsProperties.AccessibilityRangeInfo
 
 /**
  * Whether this semantics node is disabled.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt
index efbe593..f40d31b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt
@@ -17,10 +17,8 @@
 package androidx.compose.ui.semantics
 
 import androidx.compose.ui.node.DelegatingLayoutNodeWrapper
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNodeWrapper
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class SemanticsWrapper(
     wrapped: LayoutNodeWrapper,
     semanticsModifier: SemanticsModifier
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
index 323a091..9a82047 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
@@ -21,36 +21,72 @@
 import androidx.compose.ui.window.MenuBar
 import java.awt.image.BufferedImage
 
+/**
+ * AppFrame is an abstract class that represents a window.
+ * <p>
+ * Known subclasses: AppWindow
+ */
 abstract class AppFrame {
 
+    /**
+     * Gets ComposeWindow object.
+     */
     abstract val window: ComposeWindow
 
     internal var menuBar: MenuBar? = null
 
+    /**
+     * Gets the parent window. If the value is not null, the current window is a dialog.
+     */
     var invoker: AppFrame? = null
         protected set
 
+    /**
+     * Gets the title of the window. The title is displayed in the windows's native border.
+     */
     val title: String
         get() = window.title
 
+    /**
+     * Gets the width of the window.
+     */
     val width: Int
         get() = window.width
 
+    /**
+     * Gets the height of the window.
+     */
     val height: Int
         get() = window.height
 
+    /**
+     * Gets the current x coordinate of the window.
+     */
     val x: Int
         get() = window.x
 
+    /**
+     * Gets the current y coordinate of the window.
+     */
     val y: Int
         get() = window.y
 
+    /**
+     * Returns true if the winodw is closed, false otherwise.
+     */
     var isClosed: Boolean = false
         internal set
 
+    /**
+     * Returns the icon image of the window. If the icon is not set, null is returned.
+     */
     var icon: BufferedImage? = null
         internal set
 
+    /**
+     * Returns events of the window. Each event is described as a lambda that is invoked when
+     * needed.
+     */
     var events: WindowEvents = WindowEvents()
         internal set
 
@@ -58,22 +94,64 @@
 
     internal var onDismiss: (() -> Unit)? = null
 
+    /**
+     * Sets the title of the window.
+     *
+     * @param title Window title text.
+     */
     abstract fun setTitle(title: String)
 
+    /**
+     * Sets the image icon of the window.
+     *
+     * @param image Image of the icon.
+     */
     abstract fun setIcon(image: BufferedImage?)
 
+    /**
+     * Sets the menu bar of the window. The menu bar can be displayed inside a window (Windows,
+     * Linux) or at the top of the screen (Mac OS).
+     *
+     * @param manuBar Window menu bar.
+     */
     abstract fun setMenuBar(menuBar: MenuBar)
 
+    /**
+     * Removes the menu bar of the window.
+     */
     abstract fun removeMenuBar()
 
+    /**
+     * Sets the new position of the window on the screen.
+     *
+     * @param x the new x-coordinate of the window.
+     * @param y the new y-coordinate of the window.
+     */
     abstract fun setLocation(x: Int, y: Int)
 
+    /**
+     * Sets the window to the center of the screen.
+     */
     abstract fun setWindowCentered()
 
+    /**
+     * Sets the new size of the window.
+     *
+     * @param width the new width of the window.
+     * @param height the new height of the window.
+     */
     abstract fun setSize(width: Int, height: Int)
 
+    /**
+     * Shows a window with the given Compose content.
+     *
+     * @param content Composable content of the window.
+     */
     abstract fun show(content: @Composable () -> Unit)
 
+    /**
+     * Closes the window.
+     */
     abstract fun close()
 
     internal abstract fun dispose()
@@ -87,6 +165,20 @@
     internal abstract fun unlockWindow()
 }
 
+/**
+ * WindowEvents is the class that contains all the events supported by window.
+ *
+ * @param onOpen The event that is invoked after the window appears.
+ * @param onClose The event that is invoked after the window is closed.
+ * @param onMinimize The event that is invoked after the window is minimized.
+ * @param onMaximize The event that is invoked after the window is maximized.
+ * @param onRestore The event that is invoked when the window is restored after the window has been
+ * maximized or minimized.
+ * @param onFocusGet The event that is invoked when the window gets focus.
+ * @param onFocusLost The event that is invoked when the window lost focus.
+ * @param onResize The event that is invoked when the window is resized.
+ * @param onRelocate The event that is invoked when the window is relocated.
+ */
 class WindowEvents(
     var onOpen: (() -> Unit)? = null,
     var onClose: (() -> Unit)? = null,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
index d154ee8..872ad8b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
@@ -19,7 +19,6 @@
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.ambientOf
 import androidx.compose.runtime.emptyContent
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.platform.Keyboard
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -89,6 +88,9 @@
  */
 class AppWindow : AppFrame {
 
+    /**
+     * Gets ComposeWindow object.
+     */
     override val window: ComposeWindow
 
     init {
@@ -232,10 +234,20 @@
         pair = null
     }
 
+    /**
+     * Sets the title of the window.
+     *
+     * @param title Window title text.
+     */
     override fun setTitle(title: String) {
         window.setTitle(title)
     }
 
+    /**
+     * Sets the image icon of the window.
+     *
+     * @param image Image of the icon.
+     */
     override fun setIcon(image: BufferedImage?) {
         this.icon = image
         if (icon != null) {
@@ -249,16 +261,31 @@
         }
     }
 
+    /**
+     * Sets the menu bar of the window. The menu bar can be displayed inside a window (Windows,
+     * Linux) or at the top of the screen (Mac OS).
+     *
+     * @param manuBar Window menu bar.
+     */
     override fun setMenuBar(menuBar: MenuBar) {
         this.menuBar = menuBar
         window.setJMenuBar(menuBar.menuBar)
     }
 
+    /**
+     * Removes the menu bar of the window.
+     */
     override fun removeMenuBar() {
         this.menuBar = null
         window.setJMenuBar(JMenuBar())
     }
 
+    /**
+     * Sets the new size of the window.
+     *
+     * @param width the new width of the window.
+     * @param height the new height of the window.
+     */
     override fun setSize(width: Int, height: Int) {
         // better check min/max values of current window size
         var w = width
@@ -273,10 +300,19 @@
         window.setSize(w, h)
     }
 
+    /**
+     * Sets the new position of the window on the screen.
+     *
+     * @param x the new x-coordinate of the window.
+     * @param y the new y-coordinate of the window.
+     */
     override fun setLocation(x: Int, y: Int) {
         window.setLocation(x, y)
     }
 
+    /**
+     * Sets the window to the center of the screen.
+     */
     override fun setWindowCentered() {
         val dim: Dimension = Toolkit.getDefaultToolkit().getScreenSize()
         val x = dim.width / 2 - width / 2
@@ -293,7 +329,11 @@
         }
     }
 
-    @OptIn(ExperimentalKeyInput::class)
+    /**
+     * Shows a window with the given Compose content.
+     *
+     * @param content Composable content of the window.
+     */
     override fun show(content: @Composable () -> Unit) {
         if (invoker != null) {
             invoker!!.lockWindow()
@@ -309,15 +349,18 @@
         events.invokeOnOpen()
     }
 
+    /**
+     * Closes the window.
+     */
     override fun close() {
         window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
     }
 
-    override fun dispose() {
+    internal override fun dispose() {
         invoker?.unlockWindow()
     }
 
-    override fun lockWindow() {
+    internal override fun lockWindow() {
         window.apply {
             defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE
             setFocusableWindowState(false)
@@ -327,7 +370,7 @@
         invoker?.connectPair(this)
     }
 
-    override fun unlockWindow() {
+    internal override fun unlockWindow() {
         window.apply {
             defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
             setFocusableWindowState(true)
@@ -339,6 +382,8 @@
         disconnectPair()
     }
 
-    @ExperimentalKeyInput
+    /**
+     * Gets the Keyboard object of the window.
+     */
     val keyboard: Keyboard = Keyboard()
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
index fbf5304..7d74767 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
@@ -18,8 +18,6 @@
 
 package androidx.compose.desktop
 
-import org.jetbrains.skiko.Library
-
 /**
  * Can be called multiple times.
  *
@@ -35,16 +33,6 @@
  *     }
  * }
  */
-fun initCompose() {
-    // call object initializer only once
-    ComposeInit
-}
-
-private object ComposeInit {
-    init {
-        Library.load("/", "skiko")
-        // Until https://github.com/Kotlin/kotlinx.coroutines/issues/2039 is resolved
-        // we have to set this property manually for coroutines to work.
-        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
-    }
-}
\ No newline at end of file
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated("We don't need to init Compose explicitly now")
+fun initCompose() = Unit
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
index 3524a49..c189e14 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
@@ -16,12 +16,16 @@
 
 package androidx.compose.desktop
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.input.mouse.MouseScrollEvent
 import androidx.compose.ui.input.mouse.MouseScrollUnit
 import androidx.compose.ui.platform.DesktopComponent
+import androidx.compose.ui.platform.DesktopOwner
 import androidx.compose.ui.platform.DesktopOwners
 import androidx.compose.ui.platform.FrameDispatcher
+import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.unit.Density
 import org.jetbrains.skija.Canvas
 import org.jetbrains.skiko.HardwareLayer
@@ -137,12 +141,14 @@
 
     private fun initCanvas() {
         wrapped.addInputMethodListener(object : InputMethodListener {
-            override fun caretPositionChanged(p0: InputMethodEvent?) {
-                TODO("Implement input method caret change")
+            override fun caretPositionChanged(event: InputMethodEvent?) {
+                if (event != null) {
+                    owners?.onInputMethodEvent(event)
+                }
             }
 
             override fun inputMethodTextChanged(event: InputMethodEvent) = events.post {
-                owners?.onInputMethodTextChanged(event)
+                owners?.onInputMethodEvent(event)
             }
         })
 
@@ -277,7 +283,7 @@
         isDisposed = true
     }
 
-    fun needRedrawLayer() {
+    internal fun needRedrawLayer() {
         check(!isDisposed)
         frameDispatcher.scheduleFrame()
     }
@@ -285,4 +291,28 @@
     interface Renderer {
         suspend fun onFrame(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
     }
+
+    internal fun setContent(
+        parent: Any? = null,
+        invalidate: () -> Unit = this::needRedrawLayer,
+        content: @Composable () -> Unit
+    ): Composition {
+        check(owners == null) {
+            "Cannot setContent twice."
+        }
+        val desktopOwners = DesktopOwners(wrapped, invalidate)
+        val desktopOwner = DesktopOwner(desktopOwners, density)
+
+        owners = desktopOwners
+        val composition = desktopOwner.setContent(content)
+
+        onDensityChanged(desktopOwner::density::set)
+
+        when (parent) {
+            is AppFrame -> parent.onDispose = desktopOwner::dispose
+            is ComposePanel -> parent.onDispose = desktopOwner::dispose
+        }
+
+        return composition
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
index fa2ca73..6915ec41 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
@@ -27,12 +27,6 @@
  * ComposePanel is panel for building UI using Compose for Desktop.
  */
 class ComposePanel : JPanel {
-    companion object {
-        init {
-            initCompose()
-        }
-    }
-
     constructor() : super() {
         setLayout(GridLayout(1, 1))
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
index 84257ff..b980f90 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
@@ -15,17 +15,13 @@
  */
 package androidx.compose.desktop
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
 import java.awt.event.ComponentAdapter
 import java.awt.event.ComponentEvent
 import javax.swing.JFrame
 
 class ComposeWindow : JFrame {
-    companion object {
-        init {
-            initCompose()
-        }
-    }
-
     val parent: AppFrame
     internal val layer = ComposeLayer()
 
@@ -43,6 +39,21 @@
         })
     }
 
+    /**
+     * Sets Compose content of the ComposeWindow.
+     *
+     * @param content Composable content of the ComposeWindow.
+     *
+     * @return Composition of the content.
+     */
+    fun setContent(content: @Composable () -> Unit): Composition {
+        return layer.setContent(
+            parent = parent,
+            invalidate = this::needRedrawLayer,
+            content = content
+        )
+    }
+
     private fun updateLayer() {
         if (!isVisible) {
             return
@@ -50,7 +61,7 @@
         layer.updateLayer()
     }
 
-    fun needRedrawLayer() {
+    internal fun needRedrawLayer() {
         if (!isVisible) {
             return
         }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt
deleted file mode 100644
index 58b5578..0000000
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.compose.desktop
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Composition
-import androidx.compose.ui.platform.DesktopOwner
-import androidx.compose.ui.platform.DesktopOwners
-import androidx.compose.ui.platform.setContent
-
-/**
- * Sets Compose content of the ComposeWindow.
- *
- * @param content Composable content of the ComposeWindow.
- *
- * @return Composition of the content.
- */
-fun ComposeWindow.setContent(content: @Composable () -> Unit): Composition {
-    return this.layer.setContent(
-        parent = parent,
-        invalidate = this::needRedrawLayer,
-        content = content
-    )
-}
-
-internal fun ComposeLayer.setContent(
-    parent: Any? = null,
-    invalidate: () -> Unit = this::needRedrawLayer,
-    content: @Composable () -> Unit
-): Composition {
-    check(owners == null) {
-        "Cannot setContent twice."
-    }
-    val owners = DesktopOwners(this.wrapped, invalidate)
-    val owner = DesktopOwner(owners, density)
-    this.owners = owners
-    val composition = owner.setContent(content)
-
-    onDensityChanged(owner::density::set)
-
-    when (parent) {
-        is AppFrame -> parent.onDispose = owner::dispose
-        is ComposePanel -> parent.onDispose = owner::dispose
-    }
-
-    return composition
-}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt
index c8aeb01..af0c990 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt
@@ -20,7 +20,6 @@
 import java.awt.event.KeyEvent.KEY_RELEASED
 import java.awt.event.KeyEvent as KeyEventAwt
 
-@OptIn(ExperimentalKeyInput::class)
 internal inline class KeyEventDesktop(val keyEvent: KeyEventAwt) : KeyEvent {
 
     override val key: Key
@@ -47,19 +46,4 @@
 
     override val isShiftPressed: Boolean
         get() = keyEvent.isShiftDown
-
-    @Suppress("DEPRECATION", "OverridingDeprecatedMember")
-    override val alt: Alt
-        get() = AltDesktop(keyEvent)
-}
-
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalKeyInput::class)
-internal inline class AltDesktop(val keyEvent: KeyEventAwt) : Alt {
-
-    override val isLeftAltPressed
-        get() = keyEvent.isAltDown
-
-    override val isRightAltPressed
-        get() = keyEvent.isAltGraphDown
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt
index 0dad286..5f69dc6 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt
@@ -80,7 +80,6 @@
 
 private fun makeHandlers() = TreeMap<KeysSet, () -> Unit>()
 
-@ExperimentalKeyInput
 internal class ShortcutsInstance(
     internal var handlers: TreeMap<KeysSet, () -> Unit> = makeHandlers()
 ) {
@@ -130,15 +129,14 @@
 /**
  * [KeyEvent] handler which tracks pressed keys and triggers matched callbacks
  *
- * @see [keyInputFilter]
+ * @see [onKeyEvent]
  * @see [androidx.compose.ui.platform.Keyboard] to define window-scoped shortcuts
  */
-@ExperimentalKeyInput
 @Composable
 fun Modifier.shortcuts(builder: (ShortcutsBuilderScope).() -> Unit) = composed {
     val instance = remember { ShortcutsInstance() }
     instance.handlers = ShortcutsBuilderScope().also(builder).handlers
-    keyInputFilter(instance::process)
+    onKeyEvent(instance::process)
 }
 
 class ShortcutsBuilderScope {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index 3188112..36913bb 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -24,13 +24,12 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.autofill.Autofill
 import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusManagerImpl
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.DesktopCanvas
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyInputModifier
 import androidx.compose.ui.input.mouse.MouseScrollEvent
@@ -41,7 +40,6 @@
 import androidx.compose.ui.input.pointer.PointerMoveEventFilter
 import androidx.compose.ui.layout.RootMeasureBlocks
 import androidx.compose.ui.layout.globalBounds
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.MeasureAndLayoutDelegate
@@ -58,9 +56,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 
 @OptIn(
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class,
-    ExperimentalLayoutNodeApi::class,
+    ExperimentalComposeUiApi::class,
     ExperimentalComposeApi::class,
     InternalCoreApi::class
 )
@@ -78,6 +74,7 @@
     private val semanticsModifier = SemanticsModifierCore(
         id = SemanticsModifierCore.generateSemanticsId(),
         mergeDescendants = false,
+        clearAndSetSemantics = false,
         properties = {}
     )
 
@@ -178,9 +175,6 @@
         }
     }
 
-    override val hasPendingMeasureOrLayout
-        get() = measureAndLayoutDelegate.hasPendingMeasureOrLayout
-
     override fun createLayer(
         drawBlock: (Canvas) -> Unit,
         invalidateParentLayer: () -> Unit
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
index fb56f9a..5abcf8a 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
@@ -18,7 +18,6 @@
 import androidx.compose.runtime.Recomposer
 import androidx.compose.runtime.staticAmbientOf
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEventDesktop
 import androidx.compose.ui.input.mouse.MouseScrollEvent
 import androidx.compose.ui.input.pointer.PointerId
@@ -49,7 +48,6 @@
     }
 
     val list = LinkedHashSet<DesktopOwner>()
-    @ExperimentalKeyInput
     var keyboard: Keyboard? = null
 
     private var pointerId = 0L
@@ -156,8 +154,19 @@
         platformInputService.onKeyTyped(event.keyChar)
     }
 
-    fun onInputMethodTextChanged(event: InputMethodEvent) {
-        platformInputService.onInputMethodTextChanged(event)
+    fun onInputMethodEvent(event: InputMethodEvent) {
+        if (!event.isConsumed()) {
+            when (event.id) {
+                InputMethodEvent.INPUT_METHOD_TEXT_CHANGED -> {
+                    platformInputService.replaceInputMethodText(event)
+                    event.consume()
+                }
+                InputMethodEvent.CARET_POSITION_CHANGED -> {
+                    platformInputService.inputMethodCaretPositionChanged(event)
+                    event.consume()
+                }
+            }
+        }
     }
 
     private fun pointerInputEvent(x: Int, y: Int, down: Boolean): PointerInputEvent {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
index a455fb61..30a941e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
@@ -155,9 +155,17 @@
         }
     }
 
-    fun onInputMethodTextChanged(event: InputMethodEvent) {
+    internal fun inputMethodCaretPositionChanged(
+        @Suppress("UNUSED_PARAMETER") event: InputMethodEvent
+    ) {
+        // Which OSes and which input method could produce such events? We need to have some
+        // specific cases in mind before implementing this
+        println("DesktopInputComponent.inputMethodCaretPositionChanged")
+    }
+
+    internal fun replaceInputMethodText(event: InputMethodEvent) {
         currentInput?.let { input ->
-            if (event.caret == null) {
+            if (event.text == null) {
                 return
             }
             val committed = event.text.toStringUntil(event.committedCharacterCount)
@@ -173,12 +181,15 @@
                 ops.add(DeleteSurroundingTextInCodePointsEditOp(1, 0))
             }
 
+            // newCursorPosition == 1 leads to effectively ignoring of this parameter in EditOps
+            // processing. the cursor will be set after the inserted text.
             if (committed.isNotEmpty()) {
-                ops.add(CommitTextEditOp(committed, committed.length))
+                ops.add(CommitTextEditOp(committed, 1))
             }
             if (composing.isNotEmpty()) {
-                ops.add(SetComposingTextEditOp(composing, composing.length))
+                ops.add(SetComposingTextEditOp(composing, 1))
             }
+
             input.onEditCommand.invoke(ops)
         }
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt
index 1c150ec..e9fa62c 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.DragObserver
 import androidx.compose.ui.gesture.rawDragGestureFilter
@@ -91,7 +90,6 @@
     }
 }
 
-@OptIn(ExperimentalFocus::class)
 @Composable
 fun DesktopSelectionContainer(
     selection: Selection?,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
index 8cf64c5..ea143f0 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
@@ -25,7 +25,9 @@
 import androidx.compose.ui.selection.getCurrentSelectedText
 import androidx.compose.ui.selection.merge
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 
+@OptIn(ExperimentalTextApi::class)
 internal class DesktopSelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
     private var dragBeginPosition = Offset.Zero
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
index ce9ba4b4..71eae61 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
@@ -20,8 +20,10 @@
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.selection.Selectable
 import androidx.compose.ui.selection.SelectionRegistrar
+import androidx.compose.ui.text.ExperimentalTextApi
 
 // based on androidx.compose.ui.selection.SelectionRegistrarImpl
+@OptIn(ExperimentalTextApi::class)
 internal class DesktopSelectionRegistrar : SelectionRegistrar {
     internal var sorted: Boolean = false
 
@@ -71,12 +73,23 @@
         return selectables
     }
 
-    override fun onPositionChange() {
+    override fun notifyPositionChange() {
         sorted = false
         onPositionChangeCallback?.invoke()
     }
 
-    override fun onUpdateSelection(
+    override fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset
+    ) {
+        onUpdateSelectionCallback?.invoke(
+            layoutCoordinates,
+            startPosition,
+            startPosition
+        )
+    }
+
+    override fun notifySelectionUpdate(
         layoutCoordinates: LayoutCoordinates,
         startPosition: Offset,
         endPosition: Offset
@@ -87,4 +100,6 @@
             endPosition
         )
     }
+
+    override fun notifySelectionUpdateEnd() { /* do nothing */ }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt
index 059de86..6fea479 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt
@@ -19,14 +19,16 @@
 
 import androidx.compose.runtime.AbstractApplier
 import androidx.compose.runtime.ExperimentalComposeApi
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.LayoutNode
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 internal class DesktopUiApplier(
     root: LayoutNode
 ) : AbstractApplier<LayoutNode>(root) {
-    override fun insert(index: Int, instance: LayoutNode) {
+    override fun insertTopDown(index: Int, instance: LayoutNode) {
+        // ignored. Building tree bottom-up
+    }
+
+    override fun insertBottomUp(index: Int, instance: LayoutNode) {
         current.insertAt(index, instance)
     }
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt
index 750baf3..95b1628 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.platform
 
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeysSet
@@ -28,7 +27,6 @@
  *
  * @see [shortcuts] to setup event handlers based on the element that is in focus
  */
-@ExperimentalKeyInput
 class Keyboard {
     private val shortcutsInstance = lazy {
         ShortcutsInstance()
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
index 8555134..c94e69d 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
@@ -22,11 +22,10 @@
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.Recomposer
 import androidx.compose.runtime.compositionFor
-import androidx.compose.ui.input.key.ExperimentalKeyInput
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.node.LayoutNode
 
-@OptIn(ExperimentalComposeApi::class, ExperimentalKeyInput::class)
+@OptIn(ExperimentalComposeApi::class)
 fun DesktopOwner.setContent(content: @Composable () -> Unit): Composition {
     GlobalSnapshotManager.ensureStarted()
 
@@ -47,8 +46,7 @@
 
     return composition
 }
-
-@OptIn(ExperimentalKeyInput::class)
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 private fun ProvideDesktopAmbients(owner: DesktopOwner, content: @Composable () -> Unit) {
     Providers(
@@ -64,8 +62,8 @@
     }
 }
 
-@OptIn(ExperimentalComposeApi::class, ExperimentalLayoutNodeApi::class)
-internal actual fun actualSubcomposeInto(
+@OptIn(ExperimentalComposeApi::class)
+internal actual fun subcomposeInto(
     container: LayoutNode,
     parent: CompositionReference,
     composable: @Composable () -> Unit
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/res/vector/DesktopXmlVectorParser.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/res/vector/DesktopXmlVectorParser.kt
index 08b3ac3..70253a7 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/res/vector/DesktopXmlVectorParser.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/res/vector/DesktopXmlVectorParser.kt
@@ -19,14 +19,11 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorStop
-import androidx.compose.ui.graphics.LinearGradient
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.PathFillType
-import androidx.compose.ui.graphics.RadialGradient
-import androidx.compose.ui.graphics.ShaderBrush
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.graphics.StrokeJoin
-import androidx.compose.ui.graphics.SweepGradient
 import androidx.compose.ui.graphics.TileMode
 import androidx.compose.ui.graphics.vector.DefaultPivotX
 import androidx.compose.ui.graphics.vector.DefaultPivotY
@@ -157,13 +154,13 @@
 
 private fun parseStringBrush(str: String) = SolidColor(Color(parseColorValue(str)))
 
-private fun Element.parseElementBrush(): ShaderBrush? =
+private fun Element.parseElementBrush(): Brush? =
     childrenSequence
         .filterIsInstance<Element>()
         .find { it.nodeName == "gradient" }
         ?.parseGradient()
 
-private fun Element.parseGradient(): ShaderBrush? {
+private fun Element.parseGradient(): Brush? {
     return when (attributeOrNull(ANDROID_NS, "type")) {
         "linear" -> parseLinearGradient()
         "radial" -> parseRadialGradient()
@@ -173,26 +170,32 @@
 }
 
 @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-private fun Element.parseLinearGradient() = LinearGradient(
+private fun Element.parseLinearGradient() = Brush.linearGradient(
     colorStops = parseColorStops(),
-    startX = attributeOrNull(ANDROID_NS, "startX")?.toFloat() ?: 0f,
-    startY = attributeOrNull(ANDROID_NS, "startY")?.toFloat() ?: 0f,
-    endX = attributeOrNull(ANDROID_NS, "endX")?.toFloat() ?: 0f,
-    endY = attributeOrNull(ANDROID_NS, "endY")?.toFloat() ?: 0f,
+    start = Offset(
+        attributeOrNull(ANDROID_NS, "startX")?.toFloat() ?: 0f,
+        attributeOrNull(ANDROID_NS, "startY")?.toFloat() ?: 0f
+    ),
+    end = Offset(
+        attributeOrNull(ANDROID_NS, "endX")?.toFloat() ?: 0f,
+        attributeOrNull(ANDROID_NS, "endY")?.toFloat() ?: 0f
+    ),
     tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp
 )
 
 @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-private fun Element.parseRadialGradient() = RadialGradient(
+private fun Element.parseRadialGradient() = Brush.radialGradient(
     colorStops = parseColorStops(),
-    centerX = attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f,
-    centerY = attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f,
+    center = Offset(
+        attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f,
+        attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f
+    ),
     radius = attributeOrNull(ANDROID_NS, "gradientRadius")?.toFloat() ?: 0f,
     tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp
 )
 
 @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-private fun Element.parseSweepGradient() = SweepGradient(
+private fun Element.parseSweepGradient() = Brush.sweepGradient(
     colorStops = parseColorStops(),
     center = Offset(
         attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
index 2df9345..545b562 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
@@ -17,17 +17,20 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.onDispose
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.DesktopOwner
 import androidx.compose.ui.platform.DesktopOwnersAmbient
 import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntBounds
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.round
 
 @Composable
 internal actual fun ActualPopup(
@@ -49,6 +52,24 @@
 ) {
     val owners = DesktopOwnersAmbient.current
     val density = AmbientDensity.current
+
+    var parentBounds = remember { mutableStateOf(IntBounds(0, 0, 0, 0)) }
+
+    // getting parent bounds
+    Layout(
+        content = {},
+        modifier = Modifier.onGloballyPositioned { childCoordinates ->
+            val coordinates = childCoordinates.parentCoordinates!!
+            parentBounds.value = IntBounds(
+                coordinates.localToGlobal(Offset.Zero).round(),
+                coordinates.size
+            )
+        },
+        measureBlock = { _, _ ->
+            layout(0, 0) {}
+        }
+    )
+
     val owner = remember {
         val owner = DesktopOwner(owners, density)
         owner.setContent {
@@ -60,16 +81,24 @@
                     }
                 },
                 measureBlock = { measurables, constraints ->
-                    val width = owner.size.width
-                    val height = owner.size.height
-                    layout(width, height) {
+                    val width = constraints.maxWidth
+                    val height = constraints.maxHeight
+
+                    val windowBounds = IntBounds(
+                        left = 0,
+                        top = 0,
+                        right = width,
+                        bottom = height
+                    )
+
+                    layout(constraints.maxWidth, constraints.maxHeight) {
                         measurables.forEach {
                             val placeable = it.measure(constraints)
                             val offset = popupPositionProvider.calculatePosition(
-                                IntBounds(0, 0, width, height),
-                                IntBounds(0, 0, width, height),
-                                LayoutDirection.Ltr,
-                                IntSize(placeable.width, placeable.height)
+                                parentGlobalBounds = parentBounds.value,
+                                windowGlobalBounds = windowBounds,
+                                layoutDirection = layoutDirection,
+                                popupContentSize = IntSize(placeable.width, placeable.height)
                             )
                             placeable.place(offset.x, offset.y)
                         }
@@ -83,4 +112,4 @@
     onDispose {
         owner.dispose()
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
index 6a7f0d2..3672743 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
@@ -15,23 +15,39 @@
  */
 package androidx.compose.ui.window
 
+import org.jetbrains.skiko.Library
 import java.awt.event.ActionListener
 import java.awt.event.ActionEvent
 import javax.swing.JMenu
 import javax.swing.JMenuBar
 import javax.swing.JMenuItem
 
+/**
+ * MenuBar is a class that represents a menu bar that can be attached to a window.
+ * The menu bar can be displayed inside a window (Windows, Linux) or at the top of
+ * the screen (Mac OS).
+ */
 class MenuBar {
     internal var menuBar: JMenuBar
 
     init {
         menuBar = JMenuBar()
+        // For the MenuBar to work correctly, we need to set the skiko system properties
+        Library.load()
     }
 
+    /**
+     * Constructs a MenuBar with the given menus.
+     *
+     * @param menu MenuBar menus.
+     */
     constructor(vararg menu: Menu) {
         menu(menu)
     }
 
+    /**
+     * Adds additional menus to the MenuBar.
+     */
     fun add(vararg menu: Menu) {
         menu(menu)
     }
@@ -55,10 +71,26 @@
     }
 }
 
+/**
+ * Menu is a class that represents a menu on a menu bar.
+ */
 class Menu {
+    /**
+     * Gets the menu name.
+     */
     val name: String
+
+    /**
+     * Gets the menu items.
+     */
     val list: List<MenuItem>
 
+    /**
+     * Constructs a Menu with the given name and menu items.
+     *
+     * @param name Menu name.
+     * @param item Menu items.
+     */
     constructor(name: String, vararg item: MenuItem) {
         this.name = name
         this.list = item.asList()
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
index cfd4ae3..d83546f 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
@@ -19,11 +19,34 @@
 import java.awt.Toolkit
 import javax.swing.KeyStroke
 
+/**
+ * MenuItem is a class that represents an implementation of an item in a menu.
+ * Can be used with Menu or Tray.
+ */
 class MenuItem {
+
+    /**
+     * Gets the MenuItem name.
+     */
     val name: String
+
+    /**
+     * Gets the MenuItem action when it is clicked or a keyboard shortcut is pressed (if specified).
+     */
     val action: (() -> Unit)?
+
+    /**
+     * Gets the MenuItem shortcut.
+     */
     val shortcut: KeyStroke
 
+    /**
+     * Constructs a MenuItem with the given name, action, and keyboard shortcut.
+     *
+     * @param name MenuItem name.
+     * @param onClick MenuItem action.
+     * @param shortcut MenuItem keyboard shortcut.
+     */
     constructor(
         name: String,
         onClick: (() -> Unit)? = null,
@@ -35,6 +58,14 @@
     }
 }
 
+/**
+ * Returns KeyStroke for the given key. KeyStroke contains a OS-specific modifier
+ * (Command on Mac OS and Control on Windows and Linux) and the given key.
+ *
+ * @param key Keyboard key.
+ *
+ * @return KeyStroke for the given key.
+ */
 fun KeyStroke(key: Key): KeyStroke {
     return KeyStroke.getKeyStroke(
         key.keyCode,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
index d6e05cb..084d72b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
@@ -21,15 +21,37 @@
 import java.awt.TrayIcon
 import java.awt.TrayIcon.MessageType
 
+/**
+ * Notifier is a class that can send system notifications.
+ */
 class Notifier {
+
+    /**
+     * Sends a regular notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun notify(title: String, message: String) {
         send(title, message, MessageType.INFO)
     }
 
+    /**
+     * Sends a warning notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun warn(title: String, message: String) {
         send(title, message, MessageType.WARNING)
     }
 
+    /**
+     * Sends a error notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun error(title: String, message: String) {
         send(title, message, MessageType.ERROR)
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
index 3bb7faa..5e51ec8 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
@@ -23,6 +23,9 @@
 import java.awt.TrayIcon
 import java.awt.TrayIcon.MessageType
 
+/**
+ * Tray is class for working with the system tray.
+ */
 class Tray {
     private lateinit var trayIcon: TrayIcon
     private var init: Boolean = false
@@ -31,8 +34,16 @@
         init = SystemTray.isSupported()
     }
 
+    /**
+     * Constructs a Tray with the empty icon image.
+     */
     constructor() {}
 
+    /**
+     * Constructs a Tray with the given icon image.
+     *
+     * @param image Tray icon image.
+     */
     constructor(image: Image) {
         if (!init) {
             return
@@ -42,6 +53,12 @@
         trayIcon.setImageAutoSize(true)
     }
 
+    /**
+     * Constructs a Tray with the given icon image and tooltip.
+     *
+     * @param image Tray icon image.
+     * @param tooltip Tray icon tooltip.
+     */
     constructor(image: Image, tooltip: String) {
         if (!init) {
             return
@@ -51,6 +68,11 @@
         trayIcon.setImageAutoSize(true)
     }
 
+    /**
+     * Sets the Tray icon image.
+     *
+     * @param image Tray icon image.
+     */
     fun icon(image: Image) {
         if (!init) {
             return
@@ -63,6 +85,11 @@
         }
     }
 
+    /**
+     * Sets the Tray menu.
+     *
+     * @param item Menu items.
+     */
     fun menu(vararg item: MenuItem) {
         if (!init) {
             return
@@ -85,6 +112,12 @@
         }
     }
 
+    /**
+     * Sends a regular notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun notify(title: String, message: String) {
         if (!init) {
             return
@@ -92,6 +125,12 @@
         trayIcon.displayMessage(title, message, MessageType.INFO)
     }
 
+    /**
+     * Sends a warning notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun warn(title: String, message: String) {
         if (!init) {
             return
@@ -99,6 +138,12 @@
         trayIcon.displayMessage(title, message, MessageType.WARNING)
     }
 
+    /**
+     * Sends a error notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun error(title: String, message: String) {
         if (!init) {
             return
@@ -106,6 +151,9 @@
         trayIcon.displayMessage(title, message, MessageType.ERROR)
     }
 
+    /**
+     * Removes the tray icon from the system tray.
+     */
     fun remove() {
         try {
             SystemTray.getSystemTray().remove(trayIcon)
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
similarity index 100%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
similarity index 100%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
similarity index 97%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
index 27a16c4..078976f 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.desktop
 
-import androidx.compose.ui.test.TestThread
 import org.junit.Assert.assertTrue
 import org.junit.Test
 
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
similarity index 96%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
index f7f4eb3..f98d967 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.test
+package androidx.compose.desktop
 
 internal class TestThread(private val _run: () -> Unit) : Thread() {
     private var exception: Exception? = null
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
index 0429c05..280e4817 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
@@ -28,7 +28,6 @@
  * The [KeyEvent] is usually created by the system. This function creates an instance of
  * [KeyEvent] that can be used in tests.
  */
-@OptIn(ExperimentalKeyInput::class)
 fun keyEvent(key: Key, keyEventType: KeyEventType): KeyEvent {
     val action = when (keyEventType) {
         KeyEventType.KeyDown -> KEY_PRESSED
@@ -46,7 +45,6 @@
 /**
  * Creates [KeyEvent] of Unknown type. It wraps KEY_TYPED AWTs KeyEvent
  */
-@OptIn(ExperimentalKeyInput::class)
 fun keyTypedEvent(key: Key): KeyEvent {
     return KeyEventDesktop(
         KeyEventAwt(
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt
index 78df8af..0073746 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -37,10 +36,6 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class
-)
 class ShortcutsTest {
     @get:Rule
     val rule = createComposeRule()
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt
index 1fed94d..d743d95 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt
@@ -146,7 +146,7 @@
             )
             Box(
                 Modifier
-                    .mouseScrollFilter { event, bounds ->
+                    .mouseScrollFilter { _, _ ->
                         false
                     }
                     .size(5.dp, 10.dp)
@@ -254,7 +254,7 @@
             ) {
                 Box(
                     Modifier
-                        .mouseScrollFilter { event, bounds ->
+                        .mouseScrollFilter { _, _ ->
                             false
                         }
                         .size(5.dp, 10.dp)
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt
new file mode 100644
index 0000000..e7e98ab
--- /dev/null
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform
+
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.EditProcessor
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TextInputService
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.awt.Component
+import java.awt.event.InputMethodEvent
+import java.text.AttributedString
+
+private object DummyComponent : Component()
+
+@RunWith(JUnit4::class)
+class DesktopInputComponentTest {
+    @OptIn(InternalTextApi::class)
+    @Test
+    fun replaceInputMethodText_basic() {
+        val processor = EditProcessor()
+
+        val input = DesktopPlatformInput(DummyDesktopComponent)
+        val inputService = TextInputService(input)
+
+        val token = inputService.startInput(
+            TextFieldValue(),
+            ImeOptions.Default,
+            processor::onEditCommands,
+            {}
+        )
+
+        processor.onNewState(TextFieldValue("h"), inputService, token)
+
+        val familyEmoji = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66"
+
+        input.replaceInputMethodText(
+            InputMethodEvent(
+                DummyComponent,
+                InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
+                AttributedString(familyEmoji).iterator,
+                11,
+                null,
+                null
+            )
+        )
+
+        val buffer = processor.mBufferState
+
+        Assert.assertEquals("${familyEmoji}h", buffer.text)
+        Assert.assertEquals(TextRange(11), buffer.selection)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
index 1e5c29a..7f0aca2 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
@@ -29,7 +29,7 @@
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -39,12 +39,11 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
-import androidx.compose.ui.graphicsLayer
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.mouse.MouseScrollEvent
 import androidx.compose.ui.input.mouse.MouseScrollUnit
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -54,7 +53,6 @@
 import org.junit.Test
 
 @OptIn(
-    ExperimentalLayoutNodeApi::class,
     ExperimentalComposeApi::class,
     ExperimentalCoroutinesApi::class
 )
@@ -290,10 +288,12 @@
         var height by mutableStateOf(10.dp)
         setContent {
             Box(Modifier.padding(10.dp)) {
-                LazyColumnFor(
-                    listOf(Color.Red, Color.Green, Color.Blue, Color.Black, Color.Gray)
-                ) { color ->
-                    Box(Modifier.size(width = 30.dp, height = height).background(color))
+                LazyColumn {
+                    items(
+                        listOf(Color.Red, Color.Green, Color.Blue, Color.Black, Color.Gray)
+                    ) { color ->
+                        Box(Modifier.size(width = 30.dp, height = height).background(color))
+                    }
                 }
             }
         }
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
index e30a9da..b637b80 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
@@ -22,12 +22,12 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.TransformOrigin
-import androidx.compose.ui.graphicsLayer
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.test.TestComposeWindow
+import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
+import androidx.compose.ui.unit.dp
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
index c17175e..6c3c4b2 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
@@ -20,7 +20,6 @@
 import androidx.compose.runtime.Providers
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.test.initCompose
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
@@ -34,7 +33,6 @@
     platform: DesktopPlatform = DesktopPlatform.Linux,
     block: suspend RenderingTestScope.() -> Unit
 ) = runBlocking(Dispatchers.Main) {
-    initCompose()
     val scope = RenderingTestScope(width, height, platform)
     try {
         scope.block()
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
index fb44ef2..e697605 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
@@ -16,15 +16,15 @@
 
 package androidx.compose.ui.platform
 
-import androidx.compose.ui.TransformOrigin
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.round
-import androidx.compose.ui.test.junit4.createComposeRule
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
index 0a4c2b3..8b0058f 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.Recomposer
-import androidx.compose.runtime.SlotTable
 import androidx.compose.runtime.currentComposer
 import androidx.compose.runtime.dispatch.MonotonicFrameClock
 import androidx.compose.runtime.invalidate
@@ -211,7 +210,6 @@
     block: @Composable () -> Unit
 ): Composer<Unit> {
     return Composer(
-        SlotTable(),
         EmptyApplier(),
         recomposer
     ).apply {
@@ -221,7 +219,7 @@
             fn(this, 0)
         }
         applyChanges()
-        slotTable.verifyWellFormed()
+        verifyConsistent()
     }
 }
 
@@ -241,7 +239,10 @@
     override val current: Unit = Unit
     override fun down(node: Unit) {}
     override fun up() {}
-    override fun insert(index: Int, instance: Unit) {
+    override fun insertTopDown(index: Int, instance: Unit) {
+        error("Unexpected")
+    }
+    override fun insertBottomUp(index: Int, instance: Unit) {
         error("Unexpected")
     }
     override fun remove(index: Int, count: Int) {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt
index d54d1d1..f8ae9fa 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui
 
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.ValueElement
@@ -37,7 +36,6 @@
         isDebugInspectorInfoEnabled = false
     }
 
-    @OptIn(ExperimentalFocus::class)
     @Test
     fun testInspectorValue() {
         val focusRequester = FocusRequester()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt
index 3cbb212..6ef2547 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.view.View
 import android.view.autofill.AutofillManager
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.test.ComposeUiRobolectricTestRunner
@@ -35,6 +36,7 @@
 import org.robolectric.shadow.api.Shadow
 import android.graphics.Rect as AndroidRect
 
+@OptIn(ExperimentalComposeUiApi::class)
 @RunWith(ComposeUiRobolectricTestRunner::class)
 @Config(
     shadows = [ShadowAutofillManager::class],
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
index 0475d44..4361ec1 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.autofill
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
 import androidx.compose.ui.autofill.AutofillType.AddressCountry
 import androidx.compose.ui.autofill.AutofillType.AddressLocality
@@ -57,6 +58,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalComposeUiApi::class)
 @RunWith(JUnit4::class)
 class AndroidAutofillTypeTest {
 
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
index 4cb174c..f223b15 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
@@ -20,6 +20,7 @@
 import android.util.SparseArray
 import android.view.View
 import android.view.autofill.AutofillValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.test.ComposeUiRobolectricTestRunner
 import com.google.common.truth.Truth
@@ -29,6 +30,7 @@
 import org.robolectric.Robolectric
 import org.robolectric.annotation.Config
 
+@OptIn(ExperimentalComposeUiApi::class)
 @RunWith(ComposeUiRobolectricTestRunner::class)
 @Config(minSdk = 26)
 class AndroidPerformAutofillTest {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
index 1653cf7..499687b 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
@@ -21,6 +21,7 @@
 import android.view.ViewStructure
 import androidx.autofill.HintConstants.AUTOFILL_HINT_PERSON_NAME
 import androidx.compose.testutils.fake.FakeViewStructure
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.test.ComposeUiRobolectricTestRunner
 import com.google.common.truth.Truth.assertThat
@@ -30,6 +31,7 @@
 import org.robolectric.Robolectric
 import org.robolectric.annotation.Config
 
+@OptIn(ExperimentalComposeUiApi::class)
 @RunWith(ComposeUiRobolectricTestRunner::class)
 @Config(
     manifest = Config.NONE,
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
index c56ffcc..42294c67 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
@@ -16,11 +16,13 @@
 
 package androidx.compose.ui.autofill
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalComposeUiApi::class)
 @RunWith(JUnit4::class)
 class AutofillNodeTest {
     @Test
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusChangedModifierTest.kt
similarity index 76%
rename from compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
rename to compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusChangedModifierTest.kt
index 682eed0..85ae7c6 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusChangedModifierTest.kt
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui
+package androidx.compose.ui.focus
 
-import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.ValueElement
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
@@ -26,7 +25,7 @@
 import org.junit.Before
 import org.junit.Test
 
-class FocusObserverModifierTest {
+class FocusChangedModifierTest {
     @Before
     fun before() {
         isDebugInspectorInfoEnabled = true
@@ -37,15 +36,14 @@
         isDebugInspectorInfoEnabled = false
     }
 
-    @OptIn(ExperimentalFocus::class)
     @Test
     fun testInspectorValue() {
         val onFocusChange: (FocusState) -> Unit = {}
-        val modifier = Modifier.focusObserver(onFocusChange) as InspectableValue
-        assertThat(modifier.nameFallback).isEqualTo("focusObserver")
+        val modifier = Modifier.onFocusChanged(onFocusChange) as InspectableValue
+        assertThat(modifier.nameFallback).isEqualTo("onFocusChanged")
         assertThat(modifier.valueOverride).isNull()
         assertThat(modifier.inspectableElements.asIterable()).containsExactly(
-            ValueElement("onFocusChange", onFocusChange)
+            ValueElement("onFocusChanged", onFocusChange)
         )
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusEventModifierTest.kt
similarity index 73%
copy from compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
copy to compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusEventModifierTest.kt
index 682eed0..788320b 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusEventModifierTest.kt
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui
+package androidx.compose.ui.focus
 
-import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.ValueElement
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
@@ -26,7 +25,7 @@
 import org.junit.Before
 import org.junit.Test
 
-class FocusObserverModifierTest {
+class FocusEventModifierTest {
     @Before
     fun before() {
         isDebugInspectorInfoEnabled = true
@@ -37,15 +36,14 @@
         isDebugInspectorInfoEnabled = false
     }
 
-    @OptIn(ExperimentalFocus::class)
     @Test
     fun testInspectorValue() {
-        val onFocusChange: (FocusState) -> Unit = {}
-        val modifier = Modifier.focusObserver(onFocusChange) as InspectableValue
-        assertThat(modifier.nameFallback).isEqualTo("focusObserver")
+        val onFocusEvent: (FocusState) -> Unit = {}
+        val modifier = Modifier.onFocusEvent(onFocusEvent) as InspectableValue
+        assertThat(modifier.nameFallback).isEqualTo("onFocusEvent")
         assertThat(modifier.valueOverride).isNull()
         assertThat(modifier.inspectableElements.asIterable()).containsExactly(
-            ValueElement("onFocusChange", onFocusChange)
+            ValueElement("onFocusEvent", onFocusEvent)
         )
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
index cedded6..365dcd3 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.focus.FocusState.Captured
 import androidx.compose.ui.focus.FocusState.Disabled
 import androidx.compose.ui.focus.FocusState.Inactive
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.InnerPlaceable
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.ModifiedFocusNode
@@ -33,10 +32,6 @@
 import org.junit.runners.Parameterized
 import kotlin.jvm.JvmStatic
 
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalLayoutNodeApi::class
-)
 @RunWith(Parameterized::class)
 class FocusManagerTest(private val initialFocusState: FocusState) {
     companion object {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt
index 20cffbc..3d5fc83 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture
 
 import androidx.compose.ui.Modifier
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt
index 778a789..f45f5c0 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
 @file:Suppress("PrivatePropertyName")
 
 package androidx.compose.ui.gesture
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt
index 9d686f0..658136a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture
 
 import androidx.compose.ui.Modifier
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt
index b70b2b6..70abfca 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture.scrollorientationlocking
 
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.CustomEvent
 import androidx.compose.ui.input.pointer.CustomEventDispatcher
 import androidx.compose.ui.input.pointer.PointerEventPass
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt
index 8d26027..37139367 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalPointerInput::class)
-
 package androidx.compose.ui.gesture.scrollorientationlocking
 
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.CustomEventDispatcher
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputChange
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
index d072a85..b29c924 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
@@ -36,24 +36,22 @@
         isDebugInspectorInfoEnabled = false
     }
 
-    @OptIn(ExperimentalKeyInput::class)
     @Test
-    fun testInspectorValueForKeyInputFilter() {
+    fun testInspectorValueForKeyEvent() {
         val onKeyEvent: (KeyEvent) -> Boolean = { true }
-        val modifier = Modifier.keyInputFilter(onKeyEvent) as InspectableValue
-        assertThat(modifier.nameFallback).isEqualTo("keyInputFilter")
+        val modifier = Modifier.onKeyEvent(onKeyEvent) as InspectableValue
+        assertThat(modifier.nameFallback).isEqualTo("onKeyEvent")
         assertThat(modifier.valueOverride).isNull()
         assertThat(modifier.inspectableElements.asIterable()).containsExactly(
             ValueElement("onKeyEvent", onKeyEvent)
         )
     }
 
-    @OptIn(ExperimentalKeyInput::class)
     @Test
-    fun testInspectorValueForPreviewKeyInputFilter() {
+    fun testInspectorValueForPreviewKeyEvent() {
         val onPreviewKeyEvent: (KeyEvent) -> Boolean = { true }
-        val modifier = Modifier.previewKeyInputFilter(onPreviewKeyEvent) as InspectableValue
-        assertThat(modifier.nameFallback).isEqualTo("previewKeyInputFilter")
+        val modifier = Modifier.onPreviewKeyEvent(onPreviewKeyEvent) as InspectableValue
+        assertThat(modifier.nameFallback).isEqualTo("onPreviewKeyEvent")
         assertThat(modifier.valueOverride).isNull()
         assertThat(modifier.inspectableElements.asIterable()).containsExactly(
             ValueElement("onPreviewKeyEvent", onPreviewKeyEvent)
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 73bcf4c..825ef67 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -15,14 +15,14 @@
  */
 package androidx.compose.ui.node
 
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.autofill.Autofill
 import androidx.compose.ui.autofill.AutofillTree
 import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
@@ -30,7 +30,6 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.pointer.PointerInputFilter
 import androidx.compose.ui.input.pointer.PointerInputModifier
@@ -38,6 +37,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
@@ -66,7 +66,6 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-@OptIn(ExperimentalLayoutNodeApi::class)
 class LayoutNodeTest {
     @get:Rule
     val thrown = ExpectedException.none()!!
@@ -80,13 +79,13 @@
         val owner = MockOwner()
         node.attach(owner)
         assertEquals(owner, node.owner)
-        assertTrue(node.isAttached())
+        assertTrue(node.isAttached)
 
         assertEquals(1, owner.onAttachParams.count { it === node })
 
         node.detach()
         assertNull(node.owner)
-        assertFalse(node.isAttached())
+        assertFalse(node.isAttached)
         assertEquals(1, owner.onDetachParams.count { it === node })
     }
 
@@ -1634,12 +1633,50 @@
         // Dispose
         root.removeAt(0, 1)
 
-        assertFalse(node1.isAttached())
-        assertFalse(node2.isAttached())
+        assertFalse(node1.isAttached)
+        assertFalse(node2.isAttached)
         assertEquals(0, owner.onRequestMeasureParams.count { it === node1 })
         assertEquals(0, owner.onRequestMeasureParams.count { it === node2 })
     }
 
+    @Test
+    fun modifierMatchesWrapperWithIdentity() {
+        val modifier1 = Modifier.layout { measurable, constraints ->
+            val placeable = measurable.measure(constraints)
+            layout(placeable.width, placeable.height) {
+                placeable.place(0, 0)
+            }
+        }
+        val modifier2 = Modifier.layout { measurable, constraints ->
+            val placeable = measurable.measure(constraints)
+            layout(placeable.width, placeable.height) {
+                placeable.place(1, 1)
+            }
+        }
+
+        val root = LayoutNode()
+        root.modifier = modifier1.then(modifier2)
+
+        val wrapper1 = root.outerLayoutNodeWrapper
+        val wrapper2 = root.outerLayoutNodeWrapper.wrapped
+
+        assertEquals(modifier1, (wrapper1 as DelegatingLayoutNodeWrapper<*>).modifier)
+        assertEquals(modifier2, (wrapper2 as DelegatingLayoutNodeWrapper<*>).modifier)
+
+        root.modifier = modifier2.then(modifier1)
+
+        assertEquals(wrapper2, root.outerLayoutNodeWrapper)
+        assertEquals(wrapper1, root.outerLayoutNodeWrapper.wrapped)
+        assertEquals(
+            modifier1,
+            (root.outerLayoutNodeWrapper.wrapped as DelegatingLayoutNodeWrapper<*>).modifier
+        )
+        assertEquals(
+            modifier2,
+            (root.outerLayoutNodeWrapper as DelegatingLayoutNodeWrapper<*>).modifier
+        )
+    }
+
     private fun createSimpleLayout(): Triple<LayoutNode, LayoutNode, LayoutNode> {
         val layoutNode = ZeroSizedLayoutNode()
         val child1 = ZeroSizedLayoutNode()
@@ -1655,11 +1692,7 @@
         PointerInputModifier
 }
 
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalLayoutNodeApi::class,
-    InternalCoreApi::class
-)
+@OptIn(InternalCoreApi::class)
 private class MockOwner(
     val position: IntOffset = IntOffset.Zero,
     override val root: LayoutNode = LayoutNode()
@@ -1674,8 +1707,10 @@
         get() = TODO("Not yet implemented")
     override val textToolbar: TextToolbar
         get() = TODO("Not yet implemented")
+    @OptIn(ExperimentalComposeUiApi::class)
     override val autofillTree: AutofillTree
         get() = TODO("Not yet implemented")
+    @OptIn(ExperimentalComposeUiApi::class)
     override val autofill: Autofill?
         get() = TODO("Not yet implemented")
     override val density: Density
@@ -1704,8 +1739,6 @@
         layoutNode.layoutState = LayoutNode.LayoutState.NeedsRelayout
     }
 
-    override val hasPendingMeasureOrLayout = false
-
     override fun onAttach(node: LayoutNode) {
         onAttachParams += node
     }
@@ -1718,7 +1751,6 @@
 
     override fun requestFocus(): Boolean = false
 
-    @ExperimentalKeyInput
     override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
 
     override fun measureAndLayout() {
@@ -1781,7 +1813,6 @@
         get() = TODO("Not yet implemented")
 }
 
-@OptIn(ExperimentalLayoutNodeApi::class)
 fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
     LayoutNode().apply {
         this.modifier = modifier
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
index ede87ef..baa520a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
@@ -20,7 +20,9 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 
+@OptIn(ExperimentalTextApi::class)
 class MockSelectable(
     var getSelectionValue: Selection? = null,
     var getHandlePositionValue: Offset = Offset.Zero,
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
index 323adf5..b066747 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.any
@@ -32,6 +33,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionManagerDragTest {
     private val selectionRegistrar = SelectionRegistrarImpl()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
index ff70baa..18ce38e 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
@@ -24,6 +24,7 @@
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.length
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.text.subSequence
@@ -42,6 +43,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionManagerTest {
     private val selectionRegistrar = spy(SelectionRegistrarImpl())
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
index 130db56..4bf9891a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.whenever
@@ -25,6 +26,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionRegistrarImplTest {
     @Test
@@ -188,7 +190,7 @@
         assertThat(selectionRegistrar.sorted).isTrue()
 
         // Act.
-        selectionRegistrar.onPositionChange()
+        selectionRegistrar.notifyPositionChange()
 
         // Assert.
         assertThat(selectionRegistrar.sorted).isFalse()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
index 5febaab..6afcab9 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.selection
 
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import com.google.common.truth.Truth.assertThat
@@ -24,6 +25,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionTest {
     @Test
diff --git a/concurrent/futures-ktx/src/main/java/androidx/concurrent/futures/ListenableFuture.kt b/concurrent/futures-ktx/src/main/java/androidx/concurrent/futures/ListenableFuture.kt
index ddde289..8bf37d1 100644
--- a/concurrent/futures-ktx/src/main/java/androidx/concurrent/futures/ListenableFuture.kt
+++ b/concurrent/futures-ktx/src/main/java/androidx/concurrent/futures/ListenableFuture.kt
@@ -100,6 +100,6 @@
  * If this !! throws [NullPointerException], a Future is breaking its interface contract and losing
  * state - a serious fundamental bug.
  */
-private fun ExecutionException.nonNullCause(): Throwable {
+internal fun ExecutionException.nonNullCause(): Throwable {
     return this.cause!!
-}
\ No newline at end of file
+}
diff --git a/core/core-appdigest/api/current.txt b/core/core-appdigest/api/current.txt
index 6272e90..225b9a4 100644
--- a/core/core-appdigest/api/current.txt
+++ b/core/core-appdigest/api/current.txt
@@ -18,7 +18,7 @@
 
   public final class Checksums {
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.core.appdigest.Checksum![]!> getChecksums(android.content.Context, String, boolean, int, java.util.List<java.security.cert.Certificate!>, java.util.concurrent.Executor) throws java.security.cert.CertificateEncodingException, android.content.pm.PackageManager.NameNotFoundException;
-    field public static final java.util.List<java.security.cert.Certificate!>? TRUST_ALL;
+    field public static final java.util.List<java.security.cert.Certificate!> TRUST_ALL;
     field public static final java.util.List<java.security.cert.Certificate!> TRUST_NONE;
   }
 
diff --git a/core/core-appdigest/api/public_plus_experimental_current.txt b/core/core-appdigest/api/public_plus_experimental_current.txt
index 6272e90..225b9a4 100644
--- a/core/core-appdigest/api/public_plus_experimental_current.txt
+++ b/core/core-appdigest/api/public_plus_experimental_current.txt
@@ -18,7 +18,7 @@
 
   public final class Checksums {
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.core.appdigest.Checksum![]!> getChecksums(android.content.Context, String, boolean, int, java.util.List<java.security.cert.Certificate!>, java.util.concurrent.Executor) throws java.security.cert.CertificateEncodingException, android.content.pm.PackageManager.NameNotFoundException;
-    field public static final java.util.List<java.security.cert.Certificate!>? TRUST_ALL;
+    field public static final java.util.List<java.security.cert.Certificate!> TRUST_ALL;
     field public static final java.util.List<java.security.cert.Certificate!> TRUST_NONE;
   }
 
diff --git a/core/core-appdigest/api/restricted_current.txt b/core/core-appdigest/api/restricted_current.txt
index 6272e90..225b9a4 100644
--- a/core/core-appdigest/api/restricted_current.txt
+++ b/core/core-appdigest/api/restricted_current.txt
@@ -18,7 +18,7 @@
 
   public final class Checksums {
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.core.appdigest.Checksum![]!> getChecksums(android.content.Context, String, boolean, int, java.util.List<java.security.cert.Certificate!>, java.util.concurrent.Executor) throws java.security.cert.CertificateEncodingException, android.content.pm.PackageManager.NameNotFoundException;
-    field public static final java.util.List<java.security.cert.Certificate!>? TRUST_ALL;
+    field public static final java.util.List<java.security.cert.Certificate!> TRUST_ALL;
     field public static final java.util.List<java.security.cert.Certificate!> TRUST_NONE;
   }
 
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
index 2cc30c3..2046117 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
@@ -31,7 +31,6 @@
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.concurrent.futures.ResolvableFuture;
 import androidx.core.util.Preconditions;
 
@@ -60,7 +59,7 @@
      * Trust any Installer to provide checksums for the package.
      * @see #getChecksums
      */
-    public static final @Nullable List<Certificate> TRUST_ALL = Collections.singletonList(null);
+    public static final @NonNull List<Certificate> TRUST_ALL = Collections.singletonList(null);
 
     /**
      * Don't trust any Installer to provide checksums for the package.
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 2e71858..21fe8d9 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -842,8 +842,8 @@
   }
 
   public final class ShareCompat {
-    method public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
-    method public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
+    method @Deprecated public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
+    method @Deprecated public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
     method public static android.content.ComponentName? getCallingActivity(android.app.Activity);
     method public static String? getCallingPackage(android.app.Activity);
     field public static final String EXTRA_CALLING_ACTIVITY = "androidx.core.app.EXTRA_CALLING_ACTIVITY";
@@ -1524,7 +1524,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.O_MR1) public static boolean isAtLeastOMR1();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.P) public static boolean isAtLeastP();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
-    method @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
+    method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @ChecksSdkIntAtLeast(codename="S") public static boolean isAtLeastS();
   }
 
@@ -1912,6 +1912,30 @@
     method public void onActionProviderVisibilityChanged(boolean);
   }
 
+  public final class ContentInfoCompat {
+    method public android.content.ClipData getClip();
+    method public android.os.Bundle? getExtras();
+    method public int getFlags();
+    method public android.net.Uri? getLinkUri();
+    method public int getSource();
+    method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+    field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
+    field public static final int SOURCE_APP = 0; // 0x0
+    field public static final int SOURCE_CLIPBOARD = 1; // 0x1
+    field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+  }
+
+  public static final class ContentInfoCompat.Builder {
+    ctor public ContentInfoCompat.Builder(androidx.core.view.ContentInfoCompat);
+    ctor public ContentInfoCompat.Builder(android.content.ClipData, int);
+    method public androidx.core.view.ContentInfoCompat build();
+    method public androidx.core.view.ContentInfoCompat.Builder setClip(android.content.ClipData);
+    method public androidx.core.view.ContentInfoCompat.Builder setExtras(android.os.Bundle?);
+    method public androidx.core.view.ContentInfoCompat.Builder setFlags(int);
+    method public androidx.core.view.ContentInfoCompat.Builder setLinkUri(android.net.Uri?);
+    method public androidx.core.view.ContentInfoCompat.Builder setSource(int);
+  }
+
   public final class DisplayCompat {
     method public static androidx.core.view.DisplayCompat.ModeCompat![] getSupportedModes(android.content.Context, android.view.Display);
   }
@@ -2208,6 +2232,14 @@
     method public androidx.core.view.WindowInsetsCompat! onApplyWindowInsets(android.view.View!, androidx.core.view.WindowInsetsCompat!);
   }
 
+  public interface OnReceiveContentListener {
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
+  }
+
+  public interface OnReceiveContentViewBehavior {
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
+  }
+
   public final class OneShotPreDrawListener implements android.view.View.OnAttachStateChangeListener android.view.ViewTreeObserver.OnPreDrawListener {
     method public static androidx.core.view.OneShotPreDrawListener add(android.view.View, Runnable);
     method public boolean onPreDraw();
@@ -2319,6 +2351,7 @@
     method public static int getMinimumHeight(android.view.View);
     method public static int getMinimumWidth(android.view.View);
     method public static int getNextClusterForwardId(android.view.View);
+    method public static String![]? getOnReceiveContentMimeTypes(android.view.View);
     method @Deprecated public static int getOverScrollMode(android.view.View!);
     method @Px public static int getPaddingEnd(android.view.View);
     method @Px public static int getPaddingStart(android.view.View);
@@ -2372,6 +2405,7 @@
     method public static void onInitializeAccessibilityNodeInfo(android.view.View, androidx.core.view.accessibility.AccessibilityNodeInfoCompat!);
     method @Deprecated public static void onPopulateAccessibilityEvent(android.view.View!, android.view.accessibility.AccessibilityEvent!);
     method public static boolean performAccessibilityAction(android.view.View, int, android.os.Bundle!);
+    method public static androidx.core.view.ContentInfoCompat? performReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
     method public static void postInvalidateOnAnimation(android.view.View);
     method public static void postInvalidateOnAnimation(android.view.View, int, int, int, int);
     method public static void postOnAnimation(android.view.View, Runnable!);
@@ -2410,6 +2444,7 @@
     method public static void setNestedScrollingEnabled(android.view.View, boolean);
     method public static void setNextClusterForwardId(android.view.View, int);
     method public static void setOnApplyWindowInsetsListener(android.view.View, androidx.core.view.OnApplyWindowInsetsListener?);
+    method public static void setOnReceiveContentListener(android.view.View, String![]?, androidx.core.view.OnReceiveContentListener?);
     method @Deprecated public static void setOverScrollMode(android.view.View!, int);
     method public static void setPaddingRelative(android.view.View, @Px int, @Px int, @Px int, @Px int);
     method @Deprecated public static void setPivotX(android.view.View!, float);
@@ -3335,15 +3370,6 @@
     method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
   }
 
-  public abstract class RichContentReceiverCompat<T extends android.view.View> {
-    ctor public RichContentReceiverCompat();
-    method public abstract java.util.Set<java.lang.String!> getSupportedMimeTypes();
-    method public abstract boolean onReceive(T, android.content.ClipData, int, int);
-    field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
-    field public static final int SOURCE_CLIPBOARD = 0; // 0x0
-    field public static final int SOURCE_INPUT_METHOD = 1; // 0x1
-  }
-
   @Deprecated public final class ScrollerCompat {
     method @Deprecated public void abortAnimation();
     method @Deprecated public boolean computeScrollOffset();
@@ -3398,12 +3424,6 @@
     field public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; // 0x1
   }
 
-  public abstract class TextViewRichContentReceiverCompat extends androidx.core.widget.RichContentReceiverCompat<android.widget.TextView> {
-    ctor public TextViewRichContentReceiverCompat();
-    method public java.util.Set<java.lang.String!> getSupportedMimeTypes();
-    method public boolean onReceive(android.widget.TextView, android.content.ClipData, int, int);
-  }
-
   public interface TintableCompoundButton {
     method public android.content.res.ColorStateList? getSupportButtonTintList();
     method public android.graphics.PorterDuff.Mode? getSupportButtonTintMode();
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 8c72f00..c23381b 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -842,8 +842,8 @@
   }
 
   public final class ShareCompat {
-    method public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
-    method public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
+    method @Deprecated public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
+    method @Deprecated public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
     method public static android.content.ComponentName? getCallingActivity(android.app.Activity);
     method public static String? getCallingPackage(android.app.Activity);
     field public static final String EXTRA_CALLING_ACTIVITY = "androidx.core.app.EXTRA_CALLING_ACTIVITY";
@@ -1522,7 +1522,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.O_MR1) public static boolean isAtLeastOMR1();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.P) public static boolean isAtLeastP();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
-    method @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
+    method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @ChecksSdkIntAtLeast(codename="S") public static boolean isAtLeastS();
   }
 
@@ -1910,6 +1910,30 @@
     method public void onActionProviderVisibilityChanged(boolean);
   }
 
+  public final class ContentInfoCompat {
+    method public android.content.ClipData getClip();
+    method public android.os.Bundle? getExtras();
+    method public int getFlags();
+    method public android.net.Uri? getLinkUri();
+    method public int getSource();
+    method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+    field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
+    field public static final int SOURCE_APP = 0; // 0x0
+    field public static final int SOURCE_CLIPBOARD = 1; // 0x1
+    field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+  }
+
+  public static final class ContentInfoCompat.Builder {
+    ctor public ContentInfoCompat.Builder(androidx.core.view.ContentInfoCompat);
+    ctor public ContentInfoCompat.Builder(android.content.ClipData, int);
+    method public androidx.core.view.ContentInfoCompat build();
+    method public androidx.core.view.ContentInfoCompat.Builder setClip(android.content.ClipData);
+    method public androidx.core.view.ContentInfoCompat.Builder setExtras(android.os.Bundle?);
+    method public androidx.core.view.ContentInfoCompat.Builder setFlags(int);
+    method public androidx.core.view.ContentInfoCompat.Builder setLinkUri(android.net.Uri?);
+    method public androidx.core.view.ContentInfoCompat.Builder setSource(int);
+  }
+
   public final class DisplayCompat {
     method public static androidx.core.view.DisplayCompat.ModeCompat![] getSupportedModes(android.content.Context, android.view.Display);
   }
@@ -2206,6 +2230,14 @@
     method public androidx.core.view.WindowInsetsCompat! onApplyWindowInsets(android.view.View!, androidx.core.view.WindowInsetsCompat!);
   }
 
+  public interface OnReceiveContentListener {
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
+  }
+
+  public interface OnReceiveContentViewBehavior {
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
+  }
+
   public final class OneShotPreDrawListener implements android.view.View.OnAttachStateChangeListener android.view.ViewTreeObserver.OnPreDrawListener {
     method public static androidx.core.view.OneShotPreDrawListener add(android.view.View, Runnable);
     method public boolean onPreDraw();
@@ -2317,6 +2349,7 @@
     method public static int getMinimumHeight(android.view.View);
     method public static int getMinimumWidth(android.view.View);
     method public static int getNextClusterForwardId(android.view.View);
+    method public static String![]? getOnReceiveContentMimeTypes(android.view.View);
     method @Deprecated public static int getOverScrollMode(android.view.View!);
     method @Px public static int getPaddingEnd(android.view.View);
     method @Px public static int getPaddingStart(android.view.View);
@@ -2370,6 +2403,7 @@
     method public static void onInitializeAccessibilityNodeInfo(android.view.View, androidx.core.view.accessibility.AccessibilityNodeInfoCompat!);
     method @Deprecated public static void onPopulateAccessibilityEvent(android.view.View!, android.view.accessibility.AccessibilityEvent!);
     method public static boolean performAccessibilityAction(android.view.View, int, android.os.Bundle!);
+    method public static androidx.core.view.ContentInfoCompat? performReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
     method public static void postInvalidateOnAnimation(android.view.View);
     method public static void postInvalidateOnAnimation(android.view.View, int, int, int, int);
     method public static void postOnAnimation(android.view.View, Runnable!);
@@ -2408,6 +2442,7 @@
     method public static void setNestedScrollingEnabled(android.view.View, boolean);
     method public static void setNextClusterForwardId(android.view.View, int);
     method public static void setOnApplyWindowInsetsListener(android.view.View, androidx.core.view.OnApplyWindowInsetsListener?);
+    method public static void setOnReceiveContentListener(android.view.View, String![]?, androidx.core.view.OnReceiveContentListener?);
     method @Deprecated public static void setOverScrollMode(android.view.View!, int);
     method public static void setPaddingRelative(android.view.View, @Px int, @Px int, @Px int, @Px int);
     method @Deprecated public static void setPivotX(android.view.View!, float);
@@ -3333,15 +3368,6 @@
     method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
   }
 
-  public abstract class RichContentReceiverCompat<T extends android.view.View> {
-    ctor public RichContentReceiverCompat();
-    method public abstract java.util.Set<java.lang.String!> getSupportedMimeTypes();
-    method public abstract boolean onReceive(T, android.content.ClipData, int, int);
-    field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
-    field public static final int SOURCE_CLIPBOARD = 0; // 0x0
-    field public static final int SOURCE_INPUT_METHOD = 1; // 0x1
-  }
-
   @Deprecated public final class ScrollerCompat {
     method @Deprecated public void abortAnimation();
     method @Deprecated public boolean computeScrollOffset();
@@ -3396,12 +3422,6 @@
     field public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; // 0x1
   }
 
-  public abstract class TextViewRichContentReceiverCompat extends androidx.core.widget.RichContentReceiverCompat<android.widget.TextView> {
-    ctor public TextViewRichContentReceiverCompat();
-    method public java.util.Set<java.lang.String!> getSupportedMimeTypes();
-    method public boolean onReceive(android.widget.TextView, android.content.ClipData, int, int);
-  }
-
   public interface TintableCompoundButton {
     method public android.content.res.ColorStateList? getSupportButtonTintList();
     method public android.graphics.PorterDuff.Mode? getSupportButtonTintMode();
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 3c70810..25fce0b 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -945,8 +945,8 @@
   }
 
   public final class ShareCompat {
-    method public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
-    method public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
+    method @Deprecated public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
+    method @Deprecated public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
     method public static android.content.ComponentName? getCallingActivity(android.app.Activity);
     method public static String? getCallingPackage(android.app.Activity);
     field public static final String EXTRA_CALLING_ACTIVITY = "androidx.core.app.EXTRA_CALLING_ACTIVITY";
@@ -1834,7 +1834,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.O_MR1) public static boolean isAtLeastOMR1();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.P) public static boolean isAtLeastP();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
-    method @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
+    method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @ChecksSdkIntAtLeast(codename="S") public static boolean isAtLeastS();
   }
 
@@ -2227,6 +2227,7 @@
     method public static int checkArgumentInRange(int, int, int, String);
     method @IntRange(from=0) public static int checkArgumentNonnegative(int, String?);
     method @IntRange(from=0) public static int checkArgumentNonnegative(int);
+    method public static int checkFlagsArgument(int, int);
     method public static <T> T checkNotNull(T?);
     method public static <T> T checkNotNull(T?, Object);
     method public static void checkState(boolean, String?);
@@ -2295,6 +2296,36 @@
     method public void onActionProviderVisibilityChanged(boolean);
   }
 
+  public final class ContentInfoCompat {
+    method public android.content.ClipData getClip();
+    method public android.os.Bundle? getExtras();
+    method @androidx.core.view.ContentInfoCompat.Flags public int getFlags();
+    method public android.net.Uri? getLinkUri();
+    method @androidx.core.view.ContentInfoCompat.Source public int getSource();
+    method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+    field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
+    field public static final int SOURCE_APP = 0; // 0x0
+    field public static final int SOURCE_CLIPBOARD = 1; // 0x1
+    field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+  }
+
+  public static final class ContentInfoCompat.Builder {
+    ctor public ContentInfoCompat.Builder(androidx.core.view.ContentInfoCompat);
+    ctor public ContentInfoCompat.Builder(android.content.ClipData, @androidx.core.view.ContentInfoCompat.Source int);
+    method public androidx.core.view.ContentInfoCompat build();
+    method public androidx.core.view.ContentInfoCompat.Builder setClip(android.content.ClipData);
+    method public androidx.core.view.ContentInfoCompat.Builder setExtras(android.os.Bundle?);
+    method public androidx.core.view.ContentInfoCompat.Builder setFlags(@androidx.core.view.ContentInfoCompat.Flags int);
+    method public androidx.core.view.ContentInfoCompat.Builder setLinkUri(android.net.Uri?);
+    method public androidx.core.view.ContentInfoCompat.Builder setSource(@androidx.core.view.ContentInfoCompat.Source int);
+  }
+
+  @IntDef(flag=true, value={androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ContentInfoCompat.Flags {
+  }
+
+  @IntDef({androidx.core.view.ContentInfoCompat.SOURCE_APP, androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD, androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ContentInfoCompat.Source {
+  }
+
   public final class DisplayCompat {
     method public static androidx.core.view.DisplayCompat.ModeCompat![] getSupportedModes(android.content.Context, android.view.Display);
   }
@@ -2601,6 +2632,14 @@
     method public androidx.core.view.WindowInsetsCompat! onApplyWindowInsets(android.view.View!, androidx.core.view.WindowInsetsCompat!);
   }
 
+  public interface OnReceiveContentListener {
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
+  }
+
+  public interface OnReceiveContentViewBehavior {
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
+  }
+
   public final class OneShotPreDrawListener implements android.view.View.OnAttachStateChangeListener android.view.ViewTreeObserver.OnPreDrawListener {
     method public static androidx.core.view.OneShotPreDrawListener add(android.view.View, Runnable);
     method public boolean onPreDraw();
@@ -2713,6 +2752,7 @@
     method public static int getMinimumHeight(android.view.View);
     method public static int getMinimumWidth(android.view.View);
     method public static int getNextClusterForwardId(android.view.View);
+    method public static String![]? getOnReceiveContentMimeTypes(android.view.View);
     method @Deprecated public static int getOverScrollMode(android.view.View!);
     method @Px public static int getPaddingEnd(android.view.View);
     method @Px public static int getPaddingStart(android.view.View);
@@ -2766,6 +2806,7 @@
     method public static void onInitializeAccessibilityNodeInfo(android.view.View, androidx.core.view.accessibility.AccessibilityNodeInfoCompat!);
     method @Deprecated public static void onPopulateAccessibilityEvent(android.view.View!, android.view.accessibility.AccessibilityEvent!);
     method public static boolean performAccessibilityAction(android.view.View, int, android.os.Bundle!);
+    method public static androidx.core.view.ContentInfoCompat? performReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
     method public static void postInvalidateOnAnimation(android.view.View);
     method public static void postInvalidateOnAnimation(android.view.View, int, int, int, int);
     method public static void postOnAnimation(android.view.View, Runnable!);
@@ -2804,6 +2845,7 @@
     method public static void setNestedScrollingEnabled(android.view.View, boolean);
     method public static void setNextClusterForwardId(android.view.View, int);
     method public static void setOnApplyWindowInsetsListener(android.view.View, androidx.core.view.OnApplyWindowInsetsListener?);
+    method public static void setOnReceiveContentListener(android.view.View, String![]?, androidx.core.view.OnReceiveContentListener?);
     method @Deprecated public static void setOverScrollMode(android.view.View!, int);
     method public static void setPaddingRelative(android.view.View, @Px int, @Px int, @Px int, @Px int);
     method @Deprecated public static void setPivotX(android.view.View!, float);
@@ -3775,17 +3817,6 @@
     method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
   }
 
-  public abstract class RichContentReceiverCompat<T extends android.view.View> {
-    ctor public RichContentReceiverCompat();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final androidx.core.view.inputmethod.InputConnectionCompat.OnCommitContentListener buildOnCommitContentListener(T);
-    method public abstract java.util.Set<java.lang.String!> getSupportedMimeTypes();
-    method public abstract boolean onReceive(T, android.content.ClipData, int, int);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final void populateEditorInfoContentMimeTypes(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo?);
-    field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
-    field public static final int SOURCE_CLIPBOARD = 0; // 0x0
-    field public static final int SOURCE_INPUT_METHOD = 1; // 0x1
-  }
-
   @Deprecated public final class ScrollerCompat {
     method @Deprecated public void abortAnimation();
     method @Deprecated public boolean computeScrollOffset();
@@ -3844,10 +3875,9 @@
   @IntDef({androidx.core.widget.TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE, androidx.core.widget.TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface TextViewCompat.AutoSizeTextType {
   }
 
-  public abstract class TextViewRichContentReceiverCompat extends androidx.core.widget.RichContentReceiverCompat<android.widget.TextView> {
-    ctor public TextViewRichContentReceiverCompat();
-    method public java.util.Set<java.lang.String!> getSupportedMimeTypes();
-    method public boolean onReceive(android.widget.TextView, android.content.ClipData, int, int);
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class TextViewOnReceiveContentListener implements androidx.core.view.OnReceiveContentListener {
+    ctor public TextViewOnReceiveContentListener();
+    method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
   }
 
   public interface TintableCompoundButton {
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index 7f2ea99..8c91e30 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -11537,17 +11537,6 @@
 
     <issue
         id="UnsafeNewApiCall"
-        message="This call is to a method from API 16, the call containing class androidx.core.widget.TextViewRichContentReceiverCompat is not annotated with @RequiresApi(x) where x is at least 16. Either annotate the containing class with at least @RequiresApi(16) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(16)."
-        errorLine1="                    paste = clip.getItemAt(i).coerceToStyledText(context);"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java"
-            line="82"
-            column="47"/>
-    </issue>
-
-    <issue
-        id="UnsafeNewApiCall"
         message="This call is to a method from API 29, the call containing class androidx.core.os.TraceCompat is not annotated with @RequiresApi(x) where x is at least 29. Either annotate the containing class with at least @RequiresApi(29) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(29)."
         errorLine1="            return Trace.isEnabled();"
         errorLine2="                         ~~~~~~~~~">
diff --git a/core/core/src/androidTest/AndroidManifest.xml b/core/core/src/androidTest/AndroidManifest.xml
index d8170ee..9c5a7f8 100644
--- a/core/core/src/androidTest/AndroidManifest.xml
+++ b/core/core/src/androidTest/AndroidManifest.xml
@@ -34,7 +34,7 @@
 
         <activity android:name="androidx.core.widget.TextViewTestActivity"/>
 
-        <activity android:name="androidx.core.widget.RichContentReceiverTestActivity"/>
+        <activity android:name="androidx.core.widget.ReceiveContentTestActivity"/>
 
         <activity android:name="androidx.core.widget.TestContentViewActivity"/>
 
diff --git a/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java b/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java
new file mode 100644
index 0000000..3d9b90e
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PreconditionsTest {
+
+    @Test
+    public void testCheckFlagsArgument() throws Exception {
+        final int allowedFlags = (1 << 0) | (1 << 1) | (1 << 2);
+
+        int flags = 0;
+        assertThat(Preconditions.checkFlagsArgument(flags, allowedFlags)).isEqualTo(flags);
+
+        flags = (1 << 1) | (1 << 2);
+        assertThat(Preconditions.checkFlagsArgument(flags, allowedFlags)).isEqualTo(flags);
+
+        flags = (1 << 3);
+        try {
+            Preconditions.checkFlagsArgument(flags, allowedFlags);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
new file mode 100644
index 0000000..15b5fa7b
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view;
+
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ClipData;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+
+import androidx.core.util.Predicate;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ContentInfoCompatTest {
+
+    @Test
+    public void testPartition_multipleItems() throws Exception {
+        Uri sampleUri = Uri.parse("content://com.example/path");
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        clip.addItem(new ClipData.Item("Hi", "<b>Salut</b>"));
+        clip.addItem(new ClipData.Item(sampleUri));
+        ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
+                .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Test splitting when some items match and some don't.
+        Pair<ContentInfoCompat, ContentInfoCompat> split;
+        split = payload.partition(new Predicate<ClipData.Item>() {
+            @Override
+            public boolean test(ClipData.Item item) {
+                return item.getUri() != null;
+            }
+        });
+        assertThat(split.first.getClip().getItemCount()).isEqualTo(1);
+        assertThat(split.second.getClip().getItemCount()).isEqualTo(2);
+        assertThat(split.first.getClip().getItemAt(0).getUri()).isEqualTo(sampleUri);
+        assertThat(split.first.getClip().getDescription()).isNotSameInstanceAs(
+                payload.getClip().getDescription());
+        assertThat(split.second.getClip().getDescription()).isNotSameInstanceAs(
+                payload.getClip().getDescription());
+        assertThat(split.first.getSource()).isEqualTo(SOURCE_CLIPBOARD);
+        assertThat(split.first.getLinkUri()).isNotNull();
+        assertThat(split.first.getExtras()).isNotNull();
+        assertThat(split.second.getSource()).isEqualTo(SOURCE_CLIPBOARD);
+        assertThat(split.second.getLinkUri()).isNotNull();
+        assertThat(split.second.getExtras()).isNotNull();
+
+        // Test splitting when none of the items match.
+        split = payload.partition(new Predicate<ClipData.Item>() {
+            @Override
+            public boolean test(ClipData.Item item) {
+                return false;
+            }
+        });
+        assertThat(split.first).isNull();
+        assertThat(split.second).isSameInstanceAs(payload);
+
+        // Test splitting when all of the items match.
+        split = payload.partition(new Predicate<ClipData.Item>() {
+            @Override
+            public boolean test(ClipData.Item item) {
+                return true;
+            }
+        });
+        assertThat(split.first).isSameInstanceAs(payload);
+        assertThat(split.second).isNull();
+    }
+
+    @Test
+    public void testPartition_singleItem() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
+                .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        Pair<ContentInfoCompat, ContentInfoCompat> split;
+        split = payload.partition(new Predicate<ClipData.Item>() {
+            @Override
+            public boolean test(ClipData.Item item) {
+                return false;
+            }
+        });
+        assertThat(split.first).isNull();
+        assertThat(split.second).isSameInstanceAs(payload);
+
+        split = payload.partition(new Predicate<ClipData.Item>() {
+            @Override
+            public boolean test(ClipData.Item item) {
+                return true;
+            }
+        });
+        assertThat(split.first).isSameInstanceAs(payload);
+        assertThat(split.second).isNull();
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java b/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java
new file mode 100644
index 0000000..97d1d7b
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.app.Activity;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.R;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ViewCompatReceiveContentTest {
+    @Rule
+    public final ActivityTestRule<ViewCompatActivity> mActivityTestRule =
+            new ActivityTestRule<>(ViewCompatActivity.class);
+
+    private View mView;
+    private OnReceiveContentListener mMockReceiver;
+
+    @UiThreadTest
+    @Before
+    public void before() {
+        final Activity activity = mActivityTestRule.getActivity();
+        mView = activity.findViewById(androidx.core.test.R.id.view);
+        mMockReceiver = Mockito.mock(OnReceiveContentListener.class);
+    }
+
+    @UiThreadTest
+    @Test
+    public void testSetOnReceiveContentListener() throws Exception {
+        // Verify that by default the getters returns null.
+        assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isNull();
+        assertThat(getListener(mView)).isNull();
+
+        // Verify that setting non-null MIME types and a non-null receiver works.
+        String[] mimeTypes = new String[] {"image/*", "video/mp4"};
+        ViewCompat.setOnReceiveContentListener(mView, mimeTypes, mMockReceiver);
+        assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isSameInstanceAs(mimeTypes);
+        assertThat(getListener(mView)).isSameInstanceAs(mMockReceiver);
+
+        // Verify that setting null MIME types and a null receiver works.
+        ViewCompat.setOnReceiveContentListener(mView, null, null);
+        assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isNull();
+        assertThat(getListener(mView)).isNull();
+
+        // Verify that setting empty MIME types and a null receiver works.
+        ViewCompat.setOnReceiveContentListener(mView, new String[0], null);
+        assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isNull();
+        assertThat(getListener(mView)).isNull();
+
+        // Verify that setting MIME types with a null receiver works.
+        ViewCompat.setOnReceiveContentListener(mView, mimeTypes, null);
+        assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isSameInstanceAs(mimeTypes);
+        assertThat(getListener(mView)).isNull();
+
+        // Verify that setting null or empty MIME types with a non-null receiver is not allowed.
+        try {
+            ViewCompat.setOnReceiveContentListener(mView, null, mMockReceiver);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+        try {
+            ViewCompat.setOnReceiveContentListener(mView, new String[0], mMockReceiver);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+
+        // Verify that passing "*/*" as a MIME type is not allowed.
+        try {
+            ViewCompat.setOnReceiveContentListener(mView, new String[] {"image/gif", "*/*"},
+                    mMockReceiver);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+    }
+
+    @Nullable
+    private static OnReceiveContentListener getListener(@NonNull View view) {
+        return (OnReceiveContentListener) view.getTag(R.id.tag_on_receive_content_listener);
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt
index f100253..6331c51 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.core.view
 
+import android.content.Intent
 import android.content.pm.ActivityInfo
 import android.os.Build
 import android.view.View
@@ -26,6 +27,7 @@
 import androidx.core.test.R
 import androidx.core.view.WindowInsetsCompat.Type
 import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso
 import androidx.test.espresso.Espresso.onIdle
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.action.ViewActions.click
@@ -33,6 +35,7 @@
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.matcher.ViewMatchers.assertThat
 import androidx.test.espresso.matcher.ViewMatchers.hasFocus
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -181,6 +184,54 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 29)
+    @Test
+    @Ignore("IME tests are inherently flaky, but still useful for local testing.")
+    public fun ime_insets_cleared_on_back() {
+        // Test do not currently work on Cuttlefish
+        assumeNotCuttlefish()
+        assumeSoftInputMode(SOFT_INPUT_ADJUST_RESIZE)
+
+        val expectedListenerPasses = 2
+        val latch = CountDownLatch(expectedListenerPasses)
+        val received = AtomicReference<WindowInsetsCompat>()
+        val container: View = scenario.withActivity { findViewById(R.id.container) }
+
+        // Tell the window that our view will fit system windows
+        scenario.onActivity { activity ->
+            WindowCompat.setDecorFitsSystemWindows(activity.window, false)
+        }
+
+        onView(withId(R.id.edittext))
+            .perform(click())
+            .check(matches(hasFocus()))
+
+        // Set a listener to catch WindowInsets
+        ViewCompat
+            .setOnApplyWindowInsetsListener(container.rootView) { _, insets: WindowInsetsCompat ->
+                received.set(insets)
+                latch.countDown()
+                WindowInsetsCompat.CONSUMED
+            }
+
+        scenario.onActivity { activity ->
+            activity.startActivity(Intent(activity, activity::class.java))
+        }
+
+        Espresso.pressBackUnconditionally()
+        onView(withId(R.id.edittext))
+            .check(matches(isDisplayed()))
+        assertThat(
+            "OnApplyWindowListener should have been called $expectedListenerPasses times but was " +
+                "called ${expectedListenerPasses - latch.count} times",
+            latch.await(2, TimeUnit.SECONDS), `is`(true)
+        )
+
+        // Check that the IME insets is equal to 0
+        val insets = received.get()
+        assertEquals(0, insets.getInsets(Type.ime()).bottom)
+    }
+
     @SdkSuppress(minSdkVersion = 23)
     @Test
     @Ignore("IME tests are inherently flaky, but still useful for local testing.")
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt
index 8c4eb55..e17940d 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.filters.SmallTest
 import org.hamcrest.Matchers.notNullValue
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertThat
 import org.junit.Assert.assertTrue
@@ -218,19 +219,37 @@
     }
 
     @Test
-    fun test_equals() {
+    public fun test_equals() {
         val result = WindowInsetsCompat.Builder()
             .setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
             .setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
             .build()
+        result.setRootViewData(Insets.of(0, 0, 0, 15))
         val result2 = WindowInsetsCompat.Builder()
             .setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
             .setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
             .build()
+        result2.setRootViewData(Insets.of(0, 0, 0, 15))
         assertEquals(result, result2)
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 20)
+    public fun test_not_equals_root_visible_insets() {
+        val result = WindowInsetsCompat.Builder()
+            .setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
+            .setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
+            .build()
+        result.setRootViewData(Insets.of(0, 0, 0, 15))
+        val result2 = WindowInsetsCompat.Builder()
+            .setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
+            .setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
+            .build()
+        result2.setRootViewData(Insets.of(0, 0, 0, 16))
+        assertNotEquals(result, result2)
+    }
+
+    @Test
     fun test_hashCode() {
         val result = WindowInsetsCompat.Builder()
             .setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
diff --git a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverTestActivity.java b/core/core/src/androidTest/java/androidx/core/widget/ReceiveContentTestActivity.java
similarity index 85%
rename from core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverTestActivity.java
rename to core/core/src/androidTest/java/androidx/core/widget/ReceiveContentTestActivity.java
index 83a6ba0..bc5a607 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverTestActivity.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/ReceiveContentTestActivity.java
@@ -20,9 +20,9 @@
 
 import androidx.core.test.R;
 
-public class RichContentReceiverTestActivity extends BaseTestActivity {
+public class ReceiveContentTestActivity extends BaseTestActivity {
     @Override
     protected int getContentViewLayoutResId() {
-        return R.layout.rich_content_receiver_activity;
+        return R.layout.receive_content_activity;
     }
 }
diff --git a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/TextViewOnReceiveContentListenerTest.java
similarity index 73%
rename from core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverCompatTest.java
rename to core/core/src/androidTest/java/androidx/core/widget/TextViewOnReceiveContentListenerTest.java
index 6310271..a8760f0 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/TextViewOnReceiveContentListenerTest.java
@@ -16,28 +16,25 @@
 
 package androidx.core.widget;
 
-import static androidx.core.widget.RichContentReceiverCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_CLIPBOARD;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_INPUT_METHOD;
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static java.util.Collections.singleton;
-
 import android.content.ClipData;
 import android.net.Uri;
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.UnderlineSpan;
-import android.view.inputmethod.BaseInputConnection;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
-import android.widget.TextView;
 
 import androidx.core.test.R;
-import androidx.core.view.inputmethod.EditorInfoCompat;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.ContentInfoCompat.Flags;
+import androidx.core.view.ContentInfoCompat.Source;
+import androidx.core.view.OnReceiveContentListener;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
@@ -51,26 +48,20 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class RichContentReceiverCompatTest {
+public class TextViewOnReceiveContentListenerTest {
 
     @Rule
-    public final ActivityTestRule<RichContentReceiverTestActivity> mActivityTestRule =
-            new ActivityTestRule<>(RichContentReceiverTestActivity.class);
+    public final ActivityTestRule<ReceiveContentTestActivity> mActivityTestRule =
+            new ActivityTestRule<>(ReceiveContentTestActivity.class);
 
     private EditText mEditText;
-    private RichContentReceiverCompat<TextView> mReceiver;
+    private TextViewOnReceiveContentListener mReceiver;
 
     @Before
     public void before() {
-        RichContentReceiverTestActivity activity = mActivityTestRule.getActivity();
-        mEditText = activity.findViewById(R.id.edit_text_for_rich_content_receiver);
-        mReceiver = new TextViewRichContentReceiverCompat() {};
-    }
-
-    @UiThreadTest
-    @Test
-    public void testGetSupportedMimeTypes() throws Exception {
-        assertThat(mReceiver.getSupportedMimeTypes()).isEqualTo(singleton("text/*"));
+        ReceiveContentTestActivity activity = mActivityTestRule.getActivity();
+        mEditText = activity.findViewById(R.id.edit_text);
+        mReceiver = new TextViewOnReceiveContentListener();
     }
 
     @UiThreadTest
@@ -247,44 +238,12 @@
         assertTextAndCursorPosition("xz", 1);
     }
 
-    @UiThreadTest
-    @Test
-    public void testPopulateEditorInfoContentMimeTypes() throws Exception {
-        InputConnection ic = new BaseInputConnection(mEditText, true);
-        EditorInfo outAttrs = new EditorInfo();
-
-        // The field contentMimeTypes in outAttrs should be set to the MIME types of the receiver.
-        mReceiver.populateEditorInfoContentMimeTypes(ic, outAttrs);
-        assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(
-                new String[] {"text/*"});
-
-        // If the field contentMimeTypes in outAttrs already has a value assigned, it should be
-        // overwritten with the MIME types of the receiver.
-        EditorInfoCompat.setContentMimeTypes(outAttrs, new String[] {"video/mp4"});
-        mReceiver.populateEditorInfoContentMimeTypes(ic, outAttrs);
-        assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(
-                new String[] {"text/*"});
-    }
-
-    @UiThreadTest
-    @Test
-    public void testPopulateEditorInfoContentMimeTypes_nulls() throws Exception {
-        InputConnection ic = new BaseInputConnection(mEditText, true);
-        EditorInfo outAttrs = new EditorInfo();
-
-        // If the ic arg is null, outAttrs should not be populated.
-        mReceiver.populateEditorInfoContentMimeTypes(null, outAttrs);
-        assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(new String[0]);
-
-        // If the outAttrs arg is null, it should not be populated.
-        mReceiver.populateEditorInfoContentMimeTypes(ic, null);
-        assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(new String[0]);
-    }
-
-    private boolean onReceive(final RichContentReceiverCompat<TextView> receiver,
-            final ClipData clip, @RichContentReceiverCompat.Source final int source,
-            final int flags) {
-        return receiver.onReceive(mEditText, clip, source, flags);
+    private boolean onReceive(final OnReceiveContentListener receiver, ClipData clip,
+            @Source int source, @Flags int flags) {
+        ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, source)
+                .setFlags(flags)
+                .build();
+        return receiver.onReceiveContent(mEditText, payload) == null;
     }
 
     private void setTextAndCursor(final String text, final int cursorPosition) {
diff --git a/core/core/src/androidTest/res/layout/rich_content_receiver_activity.xml b/core/core/src/androidTest/res/layout/receive_content_activity.xml
similarity index 93%
rename from core/core/src/androidTest/res/layout/rich_content_receiver_activity.xml
rename to core/core/src/androidTest/res/layout/receive_content_activity.xml
index 81b6479..a4ea03c 100644
--- a/core/core/src/androidTest/res/layout/rich_content_receiver_activity.xml
+++ b/core/core/src/androidTest/res/layout/receive_content_activity.xml
@@ -21,7 +21,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <EditText
-        android:id="@+id/edit_text_for_rich_content_receiver"
+        android:id="@+id/edit_text"
         android:layout_width="200dip"
         android:layout_height="60dip" />
 </FrameLayout>
diff --git a/core/core/src/main/java/androidx/core/app/ShareCompat.java b/core/core/src/main/java/androidx/core/app/ShareCompat.java
index 79bce4d8..8d6b8ac 100644
--- a/core/core/src/main/java/androidx/core/app/ShareCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ShareCompat.java
@@ -230,7 +230,10 @@
      *
      * @param item MenuItem to configure for sharing
      * @param shareIntent IntentBuilder with data about the content to share
+     *
+     * @deprecated Use the system sharesheet. See https://developer.android.com/training/sharing/send
      */
+    @Deprecated
     public static void configureMenuItem(@NonNull MenuItem item,
             @NonNull IntentBuilder shareIntent) {
         ActionProvider itemProvider = item.getActionProvider();
@@ -259,7 +262,10 @@
      * @param menuItemId ID of the share item within menu
      * @param shareIntent IntentBuilder with data about the content to share
      * @see #configureMenuItem(MenuItem, IntentBuilder)
+     *
+     * @deprecated Use the system sharesheet. See https://developer.android.com/training/sharing/send
      */
+    @Deprecated
     public static void configureMenuItem(@NonNull Menu menu, @IdRes int menuItemId,
             @NonNull IntentBuilder shareIntent) {
         MenuItem item = menu.findItem(menuItemId);
@@ -423,12 +429,6 @@
 
         /**
          * Start a chooser activity for the current share intent.
-         *
-         * <p>Note that under most circumstances you should use
-         * {@link ShareCompat#configureMenuItem(MenuItem, IntentBuilder)
-         *  ShareCompat.configureMenuItem()} to add a Share item to the menu while
-         * presenting a detail view of the content to be shared instead
-         * of invoking this directly.</p>
          */
         public void startChooser() {
             mContext.startActivity(createChooserIntent());
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index e14d683..ba91322 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -114,18 +114,17 @@
     }
 
     /**
-     * Checks if the device is running on a pre-release version of Android R or a release
-     * version of Android R or newer.
+     * Checks if the device is running on release version of Android R or newer.
      * <p>
-     * <strong>Note:</strong> When Android R is finalized for release, this method will be
-     * deprecated and all calls should be replaced with
-     * {@code Build.VERSION.SDK_INT >= Build.VERSION_CODES.R}.
-     *
      * @return {@code true} if R APIs are available for use, {@code false} otherwise
+     * @deprecated Android R is a finalized release and this method is no longer necessary. It
+     *             will be removed in a future release of the Support Library. Instead, use
+     *             {@code Build.VERSION.SDK_INT >= Build.VERSION_CODES.R}.
      */
     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
+    @Deprecated
     public static boolean isAtLeastR() {
-        return VERSION.SDK_INT >= 30 || VERSION.CODENAME.equals("R");
+        return VERSION.SDK_INT >= 30;
     }
 
     /**
diff --git a/core/core/src/main/java/androidx/core/util/Preconditions.java b/core/core/src/main/java/androidx/core/util/Preconditions.java
index 9f8833a..9a759f8 100644
--- a/core/core/src/main/java/androidx/core/util/Preconditions.java
+++ b/core/core/src/main/java/androidx/core/util/Preconditions.java
@@ -164,6 +164,20 @@
     }
 
     /**
+     * Check the requested flags, throwing if any requested flags are outside the allowed set.
+     *
+     * @return the validated requested flags.
+     */
+    public static int checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
+        if ((requestedFlags & allowedFlags) != requestedFlags) {
+            throw new IllegalArgumentException("Requested flags 0x"
+                    + Integer.toHexString(requestedFlags) + ", but only 0x"
+                    + Integer.toHexString(allowedFlags) + " are allowed");
+        }
+        return requestedFlags;
+    }
+
+    /**
      * Ensures that that the argument numeric value is non-negative.
      *
      * @param value a numeric int value
diff --git a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
new file mode 100644
index 0000000..afa48083
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Holds all the relevant data for a request to {@link OnReceiveContentListener}.
+ */
+// This class has the "Compat" suffix because it will integrate with (ie, wrap) the SDK API once
+// that is available.
+public final class ContentInfoCompat {
+
+    /**
+     * Specifies the UI through which content is being inserted. Future versions of Android may
+     * support additional values.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    @IntDef(value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Source {
+    }
+
+    /**
+     * Specifies that the operation was triggered by the app that contains the target view.
+     */
+    public static final int SOURCE_APP = 0;
+
+    /**
+     * Specifies that the operation was triggered by a paste from the clipboard (e.g. "Paste" or
+     * "Paste as plain text" action in the insertion/selection menu).
+     */
+    public static final int SOURCE_CLIPBOARD = 1;
+
+    /**
+     * Specifies that the operation was triggered from the soft keyboard (also known as input
+     * method editor or IME). See https://developer.android.com/guide/topics/text/image-keyboard
+     * for more info.
+     */
+    public static final int SOURCE_INPUT_METHOD = 2;
+
+    /**
+     * Returns the symbolic name of the given source.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    @NonNull
+    static String sourceToString(@Source int source) {
+        switch (source) {
+            case SOURCE_APP:
+                return "SOURCE_APP";
+            case SOURCE_CLIPBOARD:
+                return "SOURCE_CLIPBOARD";
+            case SOURCE_INPUT_METHOD:
+                return "SOURCE_INPUT_METHOD";
+        }
+        return String.valueOf(source);
+    }
+
+    /**
+     * Flags to configure the insertion behavior.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    @IntDef(flag = true, value = {FLAG_CONVERT_TO_PLAIN_TEXT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Flags {
+    }
+
+    /**
+     * Flag requesting that the content should be converted to plain text prior to inserting.
+     */
+    public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1 << 0;
+
+    /**
+     * Returns the symbolic names of the set flags or {@code "0"} if no flags are set.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    @NonNull
+    static String flagsToString(@Flags int flags) {
+        if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
+            return "FLAG_CONVERT_TO_PLAIN_TEXT";
+        }
+        return String.valueOf(flags);
+    }
+
+    @NonNull
+    final ClipData mClip;
+    @Source
+    final int mSource;
+    @Flags
+    final int mFlags;
+    @Nullable
+    final Uri mLinkUri;
+    @Nullable
+    final Bundle mExtras;
+
+    ContentInfoCompat(Builder b) {
+        this.mClip = Preconditions.checkNotNull(b.mClip);
+        this.mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_INPUT_METHOD,
+                "source");
+        this.mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT);
+        this.mLinkUri = b.mLinkUri;
+        this.mExtras = b.mExtras;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "ContentInfoCompat{"
+                + "clip=" + mClip
+                + ", source=" + sourceToString(mSource)
+                + ", flags=" + flagsToString(mFlags)
+                + ", linkUri=" + mLinkUri
+                + ", extras=" + mExtras
+                + "}";
+    }
+
+    /**
+     * The data to be inserted.
+     */
+    @NonNull
+    public ClipData getClip() {
+        return mClip;
+    }
+
+    /**
+     * The source of the operation. See {@code SOURCE_} constants. Future versions of Android
+     * may pass additional values.
+     */
+    @Source
+    public int getSource() {
+        return mSource;
+    }
+
+    /**
+     * Optional flags that control the insertion behavior. See {@code FLAG_} constants.
+     */
+    @Flags
+    public int getFlags() {
+        return mFlags;
+    }
+
+    /**
+     * Optional http/https URI for the content that may be provided by the IME. This is only
+     * populated if the source is {@link #SOURCE_INPUT_METHOD} and if a non-empty
+     * {@link android.view.inputmethod.InputContentInfo#getLinkUri linkUri} was passed by the
+     * IME.
+     */
+    @Nullable
+    public Uri getLinkUri() {
+        return mLinkUri;
+    }
+
+    /**
+     * Optional additional metadata. If the source is {@link #SOURCE_INPUT_METHOD}, this will
+     * include the {@link android.view.inputmethod.InputConnection#commitContent opts} passed by
+     * the IME.
+     */
+    @Nullable
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * Partitions this content based on the given predicate.
+     *
+     * <p>This function classifies the content and organizes it into a pair, grouping the items
+     * that matched vs didn't match the predicate.
+     *
+     * <p>Except for the {@link ClipData} items, the returned objects will contain all the same
+     * metadata as this {@link ContentInfoCompat}.
+     *
+     * @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which
+     *                      partition to place it into.
+     * @return A pair containing the partitioned content. The pair's first object will have the
+     * content that matched the predicate, or null if none of the items matched. The pair's
+     * second object will have the content that didn't match the predicate, or null if all of
+     * the items matched.
+     */
+    @NonNull
+    public Pair<ContentInfoCompat, ContentInfoCompat> partition(
+            @NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
+        if (mClip.getItemCount() == 1) {
+            boolean matched = itemPredicate.test(mClip.getItemAt(0));
+            return Pair.create(matched ? this : null, matched ? null : this);
+        }
+        ArrayList<ClipData.Item> acceptedItems = new ArrayList<>();
+        ArrayList<ClipData.Item> remainingItems = new ArrayList<>();
+        for (int i = 0; i < mClip.getItemCount(); i++) {
+            ClipData.Item item = mClip.getItemAt(i);
+            if (itemPredicate.test(item)) {
+                acceptedItems.add(item);
+            } else {
+                remainingItems.add(item);
+            }
+        }
+        if (acceptedItems.isEmpty()) {
+            return Pair.create(null, this);
+        }
+        if (remainingItems.isEmpty()) {
+            return Pair.create(this, null);
+        }
+        ContentInfoCompat accepted = new Builder(this)
+                .setClip(buildClipData(mClip.getDescription(), acceptedItems))
+                .build();
+        ContentInfoCompat remaining = new Builder(this)
+                .setClip(buildClipData(mClip.getDescription(), remainingItems))
+                .build();
+        return Pair.create(accepted, remaining);
+    }
+
+    private static ClipData buildClipData(ClipDescription description,
+            List<ClipData.Item> items) {
+        ClipData clip = new ClipData(new ClipDescription(description), items.get(0));
+        for (int i = 1; i < items.size(); i++) {
+            clip.addItem(items.get(i));
+        }
+        return clip;
+    }
+
+    /**
+     * Builder for {@link ContentInfoCompat}.
+     */
+    public static final class Builder {
+        @NonNull
+        ClipData mClip;
+        @Source
+        int mSource;
+        @Flags
+        int mFlags;
+        @Nullable
+        Uri mLinkUri;
+        @Nullable
+        Bundle mExtras;
+
+        /**
+         * Creates a new builder initialized with the data from the given builder.
+         */
+        public Builder(@NonNull ContentInfoCompat other) {
+            mClip = other.mClip;
+            mSource = other.mSource;
+            mFlags = other.mFlags;
+            mLinkUri = other.mLinkUri;
+            mExtras = other.mExtras;
+        }
+
+        /**
+         * Creates a new builder.
+         *
+         * @param clip   The data to insert.
+         * @param source The source of the operation. See {@code SOURCE_} constants.
+         */
+        public Builder(@NonNull ClipData clip, @Source int source) {
+            mClip = clip;
+            mSource = source;
+        }
+
+        /**
+         * Sets the data to be inserted.
+         *
+         * @param clip The data to insert.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setClip(@NonNull ClipData clip) {
+            mClip = clip;
+            return this;
+        }
+
+        /**
+         * Sets the source of the operation.
+         *
+         * @param source The source of the operation. See {@code SOURCE_} constants.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setSource(@Source int source) {
+            mSource = source;
+            return this;
+        }
+
+        /**
+         * Sets flags that control content insertion behavior.
+         *
+         * @param flags Optional flags to configure the insertion behavior. Use 0 for default
+         *              behavior. See {@code FLAG_} constants.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setFlags(@Flags int flags) {
+            mFlags = flags;
+            return this;
+        }
+
+        /**
+         * Sets the http/https URI for the content. See
+         * {@link android.view.inputmethod.InputContentInfo#getLinkUri} for more info.
+         *
+         * @param linkUri Optional http/https URI for the content.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setLinkUri(@Nullable Uri linkUri) {
+            mLinkUri = linkUri;
+            return this;
+        }
+
+        /**
+         * Sets additional metadata.
+         *
+         * @param extras Optional bundle with additional metadata.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setExtras(@Nullable Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * @return A new {@link ContentInfoCompat} instance with the data from this builder.
+         */
+        @NonNull
+        public ContentInfoCompat build() {
+            return new ContentInfoCompat(this);
+        }
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java b/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java
new file mode 100644
index 0000000..2c42b9d
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Listener for apps to implement handling for insertion of content. Content may be both text and
+ * non-text (plain/styled text, HTML, images, videos, audio files, etc).
+ *
+ * <p>This listener can be attached to different types of UI components using
+ * {@link ViewCompat#setOnReceiveContentListener}.
+ *
+ * <p>Here is a sample implementation that handles content URIs and delegates the processing for
+ * text and everything else to the platform:<br>
+ * <pre class="prettyprint">
+ * // (1) Define the listener
+ * public class MyReceiver implements OnReceiveContentListener {
+ *     public static final String[] MIME_TYPES = new String[] {"image/*", "video/*"};
+ *
+ *     &#64;Override
+ *     public ContentInfoCompat onReceiveContent(View view, ContentInfoCompat contentInfo) {
+ *         Pair&lt;ContentInfoCompat, ContentInfoCompat&gt; split = contentInfo.partition(
+ *                 item -&gt; item.getUri() != null);
+ *         ContentInfo uriContent = split.first;
+ *         ContentInfo remaining = split.second;
+ *         if (uriContent != null) {
+ *             ClipData clip = uriContent.getClip();
+ *             for (int i = 0; i < clip.getItemCount(); i++) {
+ *                 Uri uri = clip.getItemAt(i).getUri();
+ *                 // ... app-specific logic to handle the URI ...
+ *             }
+ *         }
+ *         // Return anything that we didn't handle ourselves. This preserves the default platform
+ *         // behavior for text and anything else for which we are not implementing custom handling.
+ *         return remaining;
+ *     }
+ * }
+ *
+ * // (2) Register the listener
+ * public class MyActivity extends Activity {
+ *     &#64;Override
+ *     public void onCreate(Bundle savedInstanceState) {
+ *         // ...
+ *
+ *         AppCompatEditText myInput = findViewById(R.id.my_input);
+ *         ViewCompat.setOnReceiveContentListener(myInput, MyReceiver.MIME_TYPES, new MyReceiver());
+ *     }
+ * </pre>
+ */
+public interface OnReceiveContentListener {
+    /**
+     * Receive the given content.
+     *
+     * <p>Implementations should handle any content items of interest and return all unhandled
+     * items to preserve the default platform behavior for content that does not have app-specific
+     * handling. For example, an implementation may provide handling for content URIs (to provide
+     * support for inserting images, etc) and delegate the processing of text to the platform to
+     * preserve the common behavior for inserting text. See the class javadoc for a sample
+     * implementation and see {@link ContentInfoCompat#partition} for a convenient way to split the
+     * passed-in content.
+     *
+     * <p>If implementing handling for text: if the view has a selection, the selection should
+     * be overwritten by the passed-in content; if there's no selection, the passed-in content
+     * should be inserted at the current cursor position.
+     *
+     * <p>If implementing handling for non-text content (e.g. images): the content may be
+     * inserted inline, or it may be added as an attachment (could potentially be shown in a
+     * completely separate view).
+     *
+     * @param view The view where the content insertion was requested.
+     * @param payload The content to insert and related metadata.
+     *
+     * @return The portion of the passed-in content whose processing should be delegated to
+     * the platform. Return null if all content was handled in some way. Actual insertion of
+     * the content may be processed asynchronously in the background and may or may not
+     * succeed even if this method returns null. For example, an app may end up not inserting
+     * an item if it exceeds the app's size limit for that type of content.
+     */
+    @Nullable
+    ContentInfoCompat onReceiveContent(@NonNull View view, @NonNull ContentInfoCompat payload);
+}
diff --git a/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
new file mode 100644
index 0000000..9b67b78
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Interface for widgets to implement default behavior for receiving content. Content may be both
+ * text and non-text (plain/styled text, HTML, images, videos, audio files, etc).
+ *
+ * <p>Widgets should implement this interface to define the default behavior for receiving content.
+ * Apps wishing to provide custom behavior for receiving content should set a listener via
+ * {@link ViewCompat#setOnReceiveContentListener}. See {@link ViewCompat#performReceiveContent} for
+ * more info.
+ */
+public interface OnReceiveContentViewBehavior {
+    /**
+     * Implements a view's default behavior for receiving content.
+     *
+     * @param payload The content to insert and related metadata.
+     *
+     * @return The portion of the passed-in content that was not handled (may be all, some, or none
+     * of the passed-in content).
+     */
+    @Nullable
+    ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload);
+}
diff --git a/core/core/src/main/java/androidx/core/view/ViewCompat.java b/core/core/src/main/java/androidx/core/view/ViewCompat.java
index c5c8c38..14ac552 100644
--- a/core/core/src/main/java/androidx/core/view/ViewCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewCompat.java
@@ -22,6 +22,7 @@
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
@@ -53,6 +54,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.inputmethod.InputConnection;
 
 import androidx.annotation.FloatRange;
 import androidx.annotation.IdRes;
@@ -65,6 +67,7 @@
 import androidx.annotation.UiThread;
 import androidx.collection.SimpleArrayMap;
 import androidx.core.R;
+import androidx.core.util.Preconditions;
 import androidx.core.view.AccessibilityDelegateCompat.AccessibilityDelegateAdapter;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
@@ -78,6 +81,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -2509,12 +2513,36 @@
             }
 
             v.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
+                WindowInsetsCompat mLastInsets = null;
+                WindowInsets mReturnedInsets = null;
+
                 @Override
-                public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
+                public WindowInsets onApplyWindowInsets(final View view,
+                        final WindowInsets insets) {
                     WindowInsetsCompat compatInsets = WindowInsetsCompat
                             .toWindowInsetsCompat(insets, view);
+                    if (Build.VERSION.SDK_INT < 30) {
+                        if (compatInsets.equals(mLastInsets)) {
+                            // We got the same insets we just return the previously computed insets.
+                            return mReturnedInsets;
+                        }
+                        mLastInsets = compatInsets;
+                    }
                     compatInsets = listener.onApplyWindowInsets(view, compatInsets);
-                    return compatInsets.toWindowInsets();
+
+                    if (Build.VERSION.SDK_INT >= 30) {
+                        return compatInsets.toWindowInsets();
+                    }
+
+                    // On API < 30, the visibleInsets, used to built WindowInsetsCompat, are
+                    // updated after the insets dispatch so we don't have the updated visible
+                    // insets at that point. As a workaround, we req-apply the insets so we know
+                    // that we'll have the right value the next time it's called.
+                    requestApplyInsets(view);
+                    // Keep a copy in case the insets haven't changed on the next call so we don't
+                    // need to call the listener again.
+                    mReturnedInsets = compatInsets.toWindowInsets();
+                    return mReturnedInsets;
                 }
             });
         }
@@ -2668,6 +2696,129 @@
     }
 
     /**
+     * Sets the listener to be used to handle insertion of content into the given view.
+     *
+     * <p>Depending on the type of view, this listener may be invoked for different scenarios. For
+     * example, for an AppCompatEditText, this listener will be invoked for the following scenarios:
+     * <ol>
+     *     <li>Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the
+     *     insertion/selection menu)
+     *     <li>Content insertion from the keyboard (from {@link InputConnection#commitContent})
+     * </ol>
+     *
+     * <p>When setting a listener, clients should also declare the MIME types accepted by it.
+     * When invoked with other types of content, the listener may reject the content (defer to
+     * the default platform behavior) or execute some other fallback logic. The MIME types
+     * declared here allow different features to optionally alter their behavior. For example,
+     * the soft keyboard may choose to hide its UI for inserting GIFs for a particular input
+     * field if the MIME types set here for that field don't include "image/gif" or "image/*".
+     *
+     * <p>Note: MIME type matching in the Android framework is case-sensitive, unlike formal RFC
+     * MIME types. As a result, you should always write your MIME types with lowercase letters,
+     * or use {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to
+     * lowercase.
+     *
+     * @param view The target view.
+     * @param mimeTypes The MIME types accepted by the given listener. These may use patterns
+     *                  such as "image/*", but may not start with a wildcard. This argument must
+     *                  not be null or empty if a non-null listener is passed in.
+     * @param listener The listener to use. This can be null to reset to the default behavior.
+     */
+    public static void setOnReceiveContentListener(@NonNull View view, @Nullable String[] mimeTypes,
+            @Nullable OnReceiveContentListener listener) {
+        mimeTypes = (mimeTypes == null || mimeTypes.length == 0) ? null : mimeTypes;
+        if (listener != null) {
+            Preconditions.checkArgument(mimeTypes != null,
+                    "When the listener is set, MIME types must also be set");
+        }
+        if (mimeTypes != null) {
+            boolean hasLeadingWildcard = false;
+            for (String mimeType : mimeTypes) {
+                if (mimeType.startsWith("*")) {
+                    hasLeadingWildcard = true;
+                    break;
+                }
+            }
+            Preconditions.checkArgument(!hasLeadingWildcard,
+                    "A MIME type set here must not start with *: " + Arrays.toString(mimeTypes));
+        }
+        view.setTag(R.id.tag_on_receive_content_mime_types, mimeTypes);
+        view.setTag(R.id.tag_on_receive_content_listener, listener);
+    }
+
+    /**
+     * Returns the MIME types accepted by the listener configured on the given view via
+     * {@link #setOnReceiveContentListener}. By default returns null.
+     *
+     * <p>Different features (e.g. pasting from the clipboard, inserting stickers from the soft
+     * keyboard, etc) may optionally use this metadata to conditionally alter their behavior. For
+     * example, a soft keyboard may choose to hide its UI for inserting GIFs for a particular
+     * input field if the MIME types returned here for that field don't include "image/gif" or
+     * "image/*".
+     *
+     * <p>Note: Comparisons of MIME types should be performed using utilities such as
+     * {@link ClipDescription#compareMimeTypes} rather than simple string equality, in order to
+     * correctly handle patterns such as "text/*", "image/*", etc. Note that MIME type matching
+     * in the Android framework is case-sensitive, unlike formal RFC MIME types. As a result,
+     * you should always write your MIME types with lowercase letters, or use
+     * {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to
+     * lowercase.
+     *
+     * @param view The target view.
+     *
+     * @return The MIME types accepted by the {@link OnReceiveContentListener} for the given view
+     * (may include patterns such as "image/*").
+     */
+    @Nullable
+    public static String[] getOnReceiveContentMimeTypes(@NonNull View view) {
+        return (String[]) view.getTag(R.id.tag_on_receive_content_mime_types);
+    }
+
+    /**
+     * Receives the given content.
+     *
+     * <p>If a listener is set, invokes the listener. If the listener returns a non-null result,
+     * executes the fallback handling for the portion of the content returned by the listener.
+     *
+     * <p>If no listener is set, executes the fallback handling.
+     *
+     * <p>The fallback handling is defined by the target view if the view implements
+     * {@link OnReceiveContentViewBehavior}, or is simply a no-op.
+     *
+     * @param view The target view.
+     * @param payload The content to insert and related metadata.
+     *
+     * @return The portion of the passed-in content that was not handled (may be all, some, or none
+     * of the passed-in content).
+     */
+    @Nullable
+    public static ContentInfoCompat performReceiveContent(@NonNull View view,
+            @NonNull ContentInfoCompat payload) {
+        OnReceiveContentListener listener =
+                (OnReceiveContentListener) view.getTag(R.id.tag_on_receive_content_listener);
+        if (listener != null) {
+            ContentInfoCompat remaining = listener.onReceiveContent(view, payload);
+            return (remaining == null) ? null : getFallback(view).onReceiveContent(remaining);
+        }
+        return getFallback(view).onReceiveContent(payload);
+    }
+
+    private static OnReceiveContentViewBehavior getFallback(@NonNull View view) {
+        if (view instanceof OnReceiveContentViewBehavior) {
+            return ((OnReceiveContentViewBehavior) view);
+        }
+        return NO_OP_ON_RECEIVE_CONTENT_VIEW_BEHAVIOR;
+    }
+
+    private static final OnReceiveContentViewBehavior NO_OP_ON_RECEIVE_CONTENT_VIEW_BEHAVIOR =
+            new OnReceiveContentViewBehavior() {
+                @Override
+                public ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload) {
+                    return payload;
+                }
+            };
+
+    /**
      * Controls whether the entire hierarchy under this view will save its
      * state when a state saving traversal occurs from its parent.
      *
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
index 6e674e7..31a8a5b 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
@@ -884,7 +884,7 @@
         private Insets mSystemWindowInsets = null;
 
         private WindowInsetsCompat mRootWindowInsets;
-        private Insets mRootViewVisibleInsets;
+        Insets mRootViewVisibleInsets;
 
         Impl20(@NonNull WindowInsetsCompat host, @NonNull WindowInsets insets) {
             super(host);
@@ -1168,6 +1168,13 @@
         private static void logReflectionError(Exception e) {
             Log.e(TAG, "Failed to get visible insets. (Reflection error). " + e.getMessage(), e);
         }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!super.equals(o)) return false;
+            Impl20 impl20 = (Impl20) o;
+            return Objects.equals(mRootViewVisibleInsets, impl20.mRootViewVisibleInsets);
+        }
     }
 
     @RequiresApi(21)
@@ -1241,7 +1248,8 @@
             if (!(o instanceof Impl28)) return false;
             Impl28 otherImpl28 = (Impl28) o;
             // On API 28+ we can rely on WindowInsets.equals()
-            return Objects.equals(mPlatformInsets, otherImpl28.mPlatformInsets);
+            return Objects.equals(mPlatformInsets, otherImpl28.mPlatformInsets)
+                    && Objects.equals(mRootViewVisibleInsets, otherImpl28.mRootViewVisibleInsets);
         }
 
         @Override
diff --git a/core/core/src/main/java/androidx/core/widget/RichContentReceiverCompat.java b/core/core/src/main/java/androidx/core/widget/RichContentReceiverCompat.java
deleted file mode 100644
index 2a66954..0000000
--- a/core/core/src/main/java/androidx/core/widget/RichContentReceiverCompat.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.core.widget;
-
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.core.view.inputmethod.EditorInfoCompat;
-import androidx.core.view.inputmethod.InputConnectionCompat;
-import androidx.core.view.inputmethod.InputContentInfoCompat;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Set;
-
-/**
- * Callback for apps to implement handling for insertion of rich content. "Rich content" here refers
- * to both text and non-text content: plain text, styled text, HTML, images, videos, audio files,
- * etc.
- *
- * <p>This callback can be attached to different types of UI components. For editable
- * {@link android.widget.TextView} components, implementations should typically extend from
- * {@link TextViewRichContentReceiverCompat}.
- *
- * <p>Example implementation:<br>
- * <pre class="prettyprint">
- *   public class MyRichContentReceiver extends TextViewRichContentReceiverCompat {
- *
- *       private static final Set&lt;String&gt; SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(
- *           Set.of("text/*", "image/gif", "image/png", "image/jpg"));
- *
- *       &#64;NonNull
- *       &#64;Override
- *       public Set&lt;String&gt; getSupportedMimeTypes() {
- *           return SUPPORTED_MIME_TYPES;
- *       }
- *
- *       &#64;Override
- *       public boolean onReceive(@NonNull TextView textView, @NonNull ClipData clip,
- *               int source, int flags) {
- *         if (clip.getDescription().hasMimeType("image/*")) {
- *             return receiveImage(textView, clip);
- *         }
- *         return super.onReceive(textView, clip, source);
- *       }
- *
- *       private boolean receiveImage(@NonNull TextView textView, @NonNull ClipData clip) {
- *           // ... app-specific logic to handle the content URI in the clip ...
- *       }
- *   }
- * </pre>
- *
- * @param <T> The type of {@link View} with which this receiver can be associated.
- */
-public abstract class RichContentReceiverCompat<T extends View> {
-    private static final String TAG = "RichContentReceiver";
-
-    /**
-     * Specifies the UI through which content is being inserted.
-     */
-    @IntDef(value = {SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD})
-    @Retention(RetentionPolicy.SOURCE)
-    @interface Source {}
-
-    /**
-     * Specifies that the operation was triggered by a paste from the clipboard (e.g. "Paste" or
-     * "Paste as plain text" action in the insertion/selection menu).
-     */
-    public static final int SOURCE_CLIPBOARD = 0;
-
-    /**
-     * Specifies that the operation was triggered from the soft keyboard (also known as input method
-     * editor or IME). See https://developer.android.com/guide/topics/text/image-keyboard for more
-     * info.
-     */
-    public static final int SOURCE_INPUT_METHOD = 1;
-
-    /**
-     * Flags to configure the insertion behavior.
-     */
-    @IntDef(flag = true, value = {FLAG_CONVERT_TO_PLAIN_TEXT})
-    @Retention(RetentionPolicy.SOURCE)
-    @interface Flags {}
-
-    /**
-     * Flag for {@link #onReceive} requesting that the content should be converted to plain text
-     * prior to inserting.
-     */
-    public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1 << 0;
-
-    /**
-     * Insert the given clip.
-     *
-     * <p>For a UI component where this callback is set, this function will be invoked in the
-     * following scenarios:
-     * <ol>
-     *     <li>Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the
-     *     insertion/selection menu)
-     *     <li>Content insertion from the keyboard ({@link InputConnection#commitContent})
-     * </ol>
-     *
-     * <p>For text, if the view has a selection, the selection should be overwritten by the
-     * clip; if there's no selection, this method should insert the content at the current
-     * cursor position.
-     *
-     * <p>For rich content (e.g. an image), this function may insert the content inline, or it may
-     * add the content as an attachment (could potentially go into a completely separate view).
-     *
-     * <p>This function may be invoked with a clip whose MIME type is not in the list of supported
-     * types returned by {@link #getSupportedMimeTypes()}. This provides the opportunity to
-     * implement custom fallback logic if desired.
-     *
-     * @param view   The view where the content insertion was requested.
-     * @param clip   The clip to insert.
-     * @param source The trigger of the operation.
-     * @param flags  Optional flags to configure the insertion behavior. Use 0 for default
-     *               behavior. See {@code FLAG_} constants on this class for other options.
-     * @return Returns true if the clip was inserted.
-     */
-    public abstract boolean onReceive(@NonNull T view, @NonNull ClipData clip, @Source int source,
-            @Flags int flags);
-
-    /**
-     * Returns the MIME types that can be handled by this callback.
-     *
-     * <p>Different platform features (e.g. pasting from the clipboard, inserting stickers from the
-     * keyboard, etc) may use this function to conditionally alter their behavior. For example, the
-     * keyboard may choose to hide its UI for inserting GIFs if the input field that has focus has
-     * a {@link RichContentReceiverCompat} set and the MIME types returned from this function
-     * don't include "image/gif".
-     *
-     * @return An immutable set with the MIME types supported by this callback. The returned
-     * MIME types may contain wildcards such as "text/*", "image/*", etc.
-     */
-    @NonNull
-    public abstract Set<String> getSupportedMimeTypes();
-
-    /**
-     * Returns true if the MIME type of the given clip is {@link #getSupportedMimeTypes() supported}
-     * by this receiver.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    public final boolean supports(@NonNull ClipDescription description) {
-        for (String supportedMimeType : getSupportedMimeTypes()) {
-            if (description.hasMimeType(supportedMimeType)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Populates {@code outAttrs.contentMimeTypes} with the supported MIME types of this receiver.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    public final void populateEditorInfoContentMimeTypes(@Nullable InputConnection ic,
-            @Nullable EditorInfo outAttrs) {
-        if (ic == null || outAttrs == null) {
-            return;
-        }
-        String[] mimeTypes = getSupportedMimeTypes().toArray(new String[0]);
-        EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
-    }
-
-    /**
-     * Creates an {@link InputConnectionCompat.OnCommitContentListener} that uses this receiver
-     * to insert content. The object returned by this function should be passed to
-     * {@link InputConnectionCompat#createWrapper} when creating the {@link InputConnection} in
-     * {@link View#onCreateInputConnection}.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    @NonNull
-    public final InputConnectionCompat.OnCommitContentListener buildOnCommitContentListener(
-            @NonNull final T view) {
-        return new InputConnectionCompat.OnCommitContentListener() {
-            @Override
-            public boolean onCommitContent(InputContentInfoCompat content, int flags,
-                    Bundle opts) {
-                ClipDescription description = content.getDescription();
-                if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
-                    try {
-                        content.requestPermission();
-                    } catch (Exception e) {
-                        Log.w(TAG, "Can't insert from IME; requestPermission() failed: " + e);
-                        return false; // Can't insert the content if we don't have permission
-                    }
-                }
-                ClipData clip = new ClipData(description,
-                        new ClipData.Item(content.getContentUri()));
-                return onReceive(view, clip, SOURCE_INPUT_METHOD, 0);
-            }
-        };
-    }
-}
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
new file mode 100644
index 0000000..761cc82
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.os.Build;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spanned;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.ContentInfoCompat.Flags;
+import androidx.core.view.ContentInfoCompat.Source;
+import androidx.core.view.OnReceiveContentListener;
+
+/**
+ * Default implementation inserting content into editable {@link TextView} components. This class
+ * handles insertion of text (plain text, styled text, HTML, etc) but not images or other content.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public final class TextViewOnReceiveContentListener implements OnReceiveContentListener {
+    private static final String LOG_TAG = "ReceiveContent";
+
+    @Nullable
+    @Override
+    public ContentInfoCompat onReceiveContent(@NonNull View view,
+            @NonNull ContentInfoCompat payload) {
+        if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
+            Log.d(LOG_TAG, "onReceive: " + payload);
+        }
+        final @Source int source = payload.getSource();
+        if (source == SOURCE_INPUT_METHOD) {
+            // InputConnection.commitContent() should only be used for non-text input which is not
+            // supported by the default implementation.
+            return payload;
+        }
+
+        // The code here follows the platform logic in TextView:
+        // https://cs.android.com/android/_/android/platform/frameworks/base/+/9fefb65aa9e7beae9ca8306b925b9fbfaeffecc9:core/java/android/widget/TextView.java;l=12644
+        // In particular, multiple items within the given ClipData will trigger separate calls to
+        // replace/insert. This is to preserve the platform behavior with respect to TextWatcher
+        // notifications fired from SpannableStringBuilder when replace/insert is called.
+        final ClipData clip = payload.getClip();
+        final @Flags int flags = payload.getFlags();
+        final TextView textView = (TextView) view;
+        final Editable editable = (Editable) textView.getText();
+        final Context context = textView.getContext();
+        boolean didFirst = false;
+        for (int i = 0; i < clip.getItemCount(); i++) {
+            CharSequence paste;
+            if (Build.VERSION.SDK_INT >= 16) {
+                paste = CoerceToTextApi16Impl.coerce(context, clip.getItemAt(i), flags);
+            } else {
+                paste = CoerceToTextImpl.coerce(context, clip.getItemAt(i), flags);
+            }
+            if (paste != null) {
+                if (!didFirst) {
+                    final int selStart = Selection.getSelectionStart(editable);
+                    final int selEnd = Selection.getSelectionEnd(editable);
+                    final int start = Math.max(0, Math.min(selStart, selEnd));
+                    final int end = Math.max(0, Math.max(selStart, selEnd));
+                    Selection.setSelection(editable, end);
+                    editable.replace(start, end, paste);
+                    didFirst = true;
+                } else {
+                    editable.insert(Selection.getSelectionEnd(editable), "\n");
+                    editable.insert(Selection.getSelectionEnd(editable), paste);
+                }
+            }
+        }
+        return null;
+    }
+
+    @RequiresApi(16) // For ClipData.Item.coerceToStyledText()
+    private static final class CoerceToTextApi16Impl {
+        private CoerceToTextApi16Impl() {}
+
+        static CharSequence coerce(Context context, ClipData.Item item, @Flags int flags) {
+            if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
+                CharSequence text = item.coerceToText(context);
+                return (text instanceof Spanned) ? text.toString() : text;
+            } else {
+                return item.coerceToStyledText(context);
+            }
+        }
+    }
+
+    private static final class CoerceToTextImpl {
+        private CoerceToTextImpl() {}
+
+        static CharSequence coerce(Context context, ClipData.Item item, @Flags int flags) {
+            CharSequence text = item.coerceToText(context);
+            if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0 && text instanceof Spanned) {
+                text = text.toString();
+            }
+            return text;
+        }
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java
deleted file mode 100644
index 31c51b1..0000000
--- a/core/core/src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.core.widget;
-
-import android.content.ClipData;
-import android.content.Context;
-import android.os.Build;
-import android.text.Editable;
-import android.text.Selection;
-import android.text.Spanned;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * Base implementation of {@link RichContentReceiverCompat} for editable {@link TextView}
- * components.
- *
- * <p>This class handles insertion of text (plain text, styled text, HTML, etc) but not images or
- * other rich content. It should be used as a base class when implementing a custom
- * {@link RichContentReceiverCompat}, to provide consistent behavior for insertion of text while
- * implementing custom behavior for insertion of other content (images, etc).
- *
- * <p>See {@link RichContentReceiverCompat} for an example of how to implement a custom receiver.
- */
-public abstract class TextViewRichContentReceiverCompat extends
-        RichContentReceiverCompat<TextView> {
-
-    private static final Set<String> MIME_TYPES_ALL_TEXT = Collections.singleton("text/*");
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    @NonNull
-    public Set<String> getSupportedMimeTypes() {
-        return MIME_TYPES_ALL_TEXT;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean onReceive(@NonNull TextView textView, @NonNull ClipData clip,
-            @Source int source, @Flags int flags) {
-        if (source == SOURCE_INPUT_METHOD && !supports(clip.getDescription())) {
-            return false;
-        }
-
-        // The code here follows the platform logic in TextView:
-        // https://cs.android.com/android/_/android/platform/frameworks/base/+/9fefb65aa9e7beae9ca8306b925b9fbfaeffecc9:core/java/android/widget/TextView.java;l=12644
-        // In particular, multiple items within the given ClipData will trigger separate calls to
-        // replace/insert. This is to preserve the platform behavior with respect to TextWatcher
-        // notifications fired from SpannableStringBuilder when replace/insert is called.
-        final Editable editable = (Editable) textView.getText();
-        final Context context = textView.getContext();
-        boolean didFirst = false;
-        for (int i = 0; i < clip.getItemCount(); i++) {
-            CharSequence paste;
-            if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
-                paste = clip.getItemAt(i).coerceToText(context);
-                paste = (paste instanceof Spanned) ? paste.toString() : paste;
-            } else {
-                if (Build.VERSION.SDK_INT >= 16) {
-                    paste = clip.getItemAt(i).coerceToStyledText(context);
-                } else {
-                    paste = clip.getItemAt(i).coerceToText(context);
-                }
-            }
-            if (paste != null) {
-                if (!didFirst) {
-                    final int selStart = Selection.getSelectionStart(editable);
-                    final int selEnd = Selection.getSelectionEnd(editable);
-                    final int start = Math.max(0, Math.min(selStart, selEnd));
-                    final int end = Math.max(0, Math.max(selStart, selEnd));
-                    Selection.setSelection(editable, end);
-                    editable.replace(start, end, paste);
-                    didFirst = true;
-                } else {
-                    editable.insert(Selection.getSelectionEnd(editable), "\n");
-                    editable.insert(Selection.getSelectionEnd(editable), paste);
-                }
-            }
-        }
-        return didFirst;
-    }
-}
diff --git a/core/core/src/main/res/values/ids.xml b/core/core/src/main/res/values/ids.xml
index 5147433..b842d05 100644
--- a/core/core/src/main/res/values/ids.xml
+++ b/core/core/src/main/res/values/ids.xml
@@ -30,6 +30,8 @@
     <item name="tag_accessibility_clickable_spans" type="id"/>
     <item name="tag_accessibility_actions" type="id"/>
     <item name="tag_state_description" type="id"/>
+    <item name="tag_on_receive_content_listener" type="id"/>
+    <item name="tag_on_receive_content_mime_types" type="id"/>
     <item name="accessibility_custom_action_0" type="id"/>
     <item name="accessibility_custom_action_1" type="id"/>
     <item name="accessibility_custom_action_2" type="id"/>
diff --git a/customview/customview/api/current.txt b/customview/customview/api/current.txt
index 2834ec2..b7566cb 100644
--- a/customview/customview/api/current.txt
+++ b/customview/customview/api/current.txt
@@ -38,6 +38,7 @@
     method protected void onVirtualViewKeyboardFocusChanged(int, boolean);
     method public final boolean requestKeyboardFocusForVirtualView(int);
     method public final boolean sendEventForVirtualView(int, int);
+    method public final void setBoundsInScreenFromBoundsInParent(androidx.core.view.accessibility.AccessibilityNodeInfoCompat, android.graphics.Rect);
     field public static final int HOST_ID = -1; // 0xffffffff
     field public static final int INVALID_ID = -2147483648; // 0x80000000
   }
diff --git a/customview/customview/api/public_plus_experimental_current.txt b/customview/customview/api/public_plus_experimental_current.txt
index 2834ec2..b7566cb 100644
--- a/customview/customview/api/public_plus_experimental_current.txt
+++ b/customview/customview/api/public_plus_experimental_current.txt
@@ -38,6 +38,7 @@
     method protected void onVirtualViewKeyboardFocusChanged(int, boolean);
     method public final boolean requestKeyboardFocusForVirtualView(int);
     method public final boolean sendEventForVirtualView(int, int);
+    method public final void setBoundsInScreenFromBoundsInParent(androidx.core.view.accessibility.AccessibilityNodeInfoCompat, android.graphics.Rect);
     field public static final int HOST_ID = -1; // 0xffffffff
     field public static final int INVALID_ID = -2147483648; // 0x80000000
   }
diff --git a/customview/customview/api/restricted_current.txt b/customview/customview/api/restricted_current.txt
index 2834ec2..b7566cb 100644
--- a/customview/customview/api/restricted_current.txt
+++ b/customview/customview/api/restricted_current.txt
@@ -38,6 +38,7 @@
     method protected void onVirtualViewKeyboardFocusChanged(int, boolean);
     method public final boolean requestKeyboardFocusForVirtualView(int);
     method public final boolean sendEventForVirtualView(int, int);
+    method public final void setBoundsInScreenFromBoundsInParent(androidx.core.view.accessibility.AccessibilityNodeInfoCompat, android.graphics.Rect);
     field public static final int HOST_ID = -1; // 0xffffffff
     field public static final int INVALID_ID = -2147483648; // 0x80000000
   }
diff --git a/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java b/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java
index 1e11425..ebaa94a 100644
--- a/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java
+++ b/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java
@@ -17,11 +17,9 @@
 package androidx.customview.widget;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.os.Bundle;
 import android.view.KeyEvent;
 import android.view.View;
@@ -61,70 +59,94 @@
 
     @Test
     @UiThreadTest
-    public void testBoundsInScreen() {
-        final ExploreByTouchHelper helper = new ParentBoundsHelper(mHost);
+    public void testAssignBoundsInParent() {
+        final TwoNestedViewHelper boundsInParentOnlyHelper = new ParentBoundsHelper(mHost);
+        testBounds(boundsInParentOnlyHelper);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testAssignBoundsInScreen() {
+        final TwoNestedViewHelper boundsInScreenOnlyHelper = new ScreenBoundsHelper(mHost);
+        testBounds(boundsInScreenOnlyHelper);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testAssignBoundsInScreenAndParent() {
+        final TwoNestedViewHelper boundsInScreenAndParentHelper =
+                new ParentAndScreenBoundsHelper(mHost);
+        testBounds(boundsInScreenAndParentHelper);
+    }
+
+    private void testBounds(TwoNestedViewHelper helper) {
         ViewCompat.setAccessibilityDelegate(mHost, helper);
-
-        final AccessibilityNodeInfoCompat node =
-                helper.getAccessibilityNodeProvider(mHost).createAccessibilityNodeInfo(1);
-        assertNotNull(node);
-
-        final Rect hostBounds = new Rect();
-        mHost.getLocalVisibleRect(hostBounds);
-        assertFalse("Host has not been laid out", hostBounds.isEmpty());
-
-        final Rect nodeBoundsInParent = new Rect();
-        node.getBoundsInParent(nodeBoundsInParent);
-        assertEquals("Wrong bounds in parent", hostBounds, nodeBoundsInParent);
-
-        final Rect hostBoundsOnScreen = getBoundsOnScreen(mHost);
-        final Rect nodeBoundsInScreen = new Rect();
-        node.getBoundsInScreen(nodeBoundsInScreen);
-        assertEquals("Wrong bounds in screen", hostBoundsOnScreen, nodeBoundsInScreen);
-
-        final int scrollX = 100;
-        final int scrollY = 50;
-        mHost.scrollTo(scrollX, scrollY);
-
-        // Generate a node for the new position.
-        final AccessibilityNodeInfoCompat scrolledNode =
-                helper.getAccessibilityNodeProvider(mHost).createAccessibilityNodeInfo(1);
-        assertNotNull(scrolledNode);
-
-        // Bounds in parent should not be affected by visibility.
-        final Rect scrolledNodeBoundsInParent = new Rect();
-        scrolledNode.getBoundsInParent(scrolledNodeBoundsInParent);
-        assertEquals("Wrong bounds in parent after scrolling",
-                hostBounds, scrolledNodeBoundsInParent);
-
-        final Rect expectedBoundsInScreen = new Rect(hostBoundsOnScreen);
-        expectedBoundsInScreen.offset(-scrollX, -scrollY);
-        expectedBoundsInScreen.intersect(hostBoundsOnScreen);
-        scrolledNode.getBoundsInScreen(nodeBoundsInScreen);
-        assertEquals("Wrong bounds in screen after scrolling",
-                expectedBoundsInScreen, nodeBoundsInScreen);
-
+        testBounds(helper, 0);
+        testBounds(helper, 1);
+        mHost.scrollTo(100, 50);
+        testBounds(helper, 0);
+        testBounds(helper, 1);
+        mHost.scrollTo(0, 0);
         ViewCompat.setAccessibilityDelegate(mHost, null);
     }
+
+    private void testBounds(TwoNestedViewHelper helper, int virtualViewId) {
+        AccessibilityNodeInfoCompat node =
+                helper.getAccessibilityNodeProvider(mHost).createAccessibilityNodeInfo(
+                        virtualViewId);
+        assertNotNull(node);
+
+        VirtualItem item = helper.mVirtualItems[virtualViewId];
+        final Rect nodeBoundsInParent = new Rect();
+        node.getBoundsInParent(nodeBoundsInParent);
+        assertEquals("Wrong bounds in parent", item.mBoundsInParent, nodeBoundsInParent);
+
+        final Rect expectedNodeBoundsInScreen = getBoundsOnScreen(helper, virtualViewId,
+                item.mParentId);
+        final Rect nodeBoundsInScreen = new Rect();
+        node.getBoundsInScreen(nodeBoundsInScreen);
+        assertEquals("Wrong bounds in screen", expectedNodeBoundsInScreen,
+                nodeBoundsInScreen);
+
+        node.recycle();
+    }
+
+    private Rect getBoundsOnScreen(TwoNestedViewHelper helper, int virtualViewId,
+            int virtualParentId) {
+        final Rect boundsOnScreen = new Rect();
+        boundsOnScreen.set(helper.mVirtualItems[virtualViewId].mBoundsInParent);
+        if (virtualParentId != ExploreByTouchHelper.HOST_ID) {
+            boundsOnScreen.offset(helper.mVirtualItems[virtualParentId].mBoundsInParent.left,
+                    helper.mVirtualItems[virtualParentId].mBoundsInParent.top);
+        }
+        final int[] tempLocation = new int[2];
+        mHost.getLocationOnScreen(tempLocation);
+        boundsOnScreen.offset(tempLocation[0] - mHost.getScrollX(),
+                tempLocation[1] - mHost.getScrollY());
+        final Rect tempVisibleRect = new Rect();
+        mHost.getLocalVisibleRect(tempVisibleRect);
+        tempVisibleRect.offset(tempLocation[0] - mHost.getScrollX(),
+                tempLocation[1] - mHost.getScrollY());
+        boundsOnScreen.intersect(tempVisibleRect);
+        return boundsOnScreen;
+    }
+
     @Test
     @UiThreadTest
     public void testMoveFocusToNextVirtualId() {
-        final ExploreByTouchHelper helper = new FocusTouchHelper(mHost);
-
+        final ExploreByTouchHelper helper = new TwoNestedViewHelper(mHost);
         ViewCompat.setAccessibilityDelegate(mHost, helper);
 
+        boolean moveFocusToId0 = helper.dispatchKeyEvent(
+                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
+        assertEquals(0, helper.getKeyboardFocusedVirtualViewId());
+        assertEquals(true, moveFocusToId0);
+
         boolean moveFocusToId1 = helper.dispatchKeyEvent(
                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
         assertEquals(1, helper.getKeyboardFocusedVirtualViewId());
         assertEquals(true, moveFocusToId1);
 
-        // moveFocus should move focus to the node with id 5
-        boolean moveFocusToId5 = helper.dispatchKeyEvent(
-                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
-        assertEquals(5, helper.getKeyboardFocusedVirtualViewId());
-        assertEquals(true, moveFocusToId5);
-
-        // moveFocus should not return true if the node has id INVALID_ID.
         boolean moveFocusToInvalidId = helper.dispatchKeyEvent(
                 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
         assertEquals(ExploreByTouchHelper.INVALID_ID, helper.getKeyboardFocusedVirtualViewId());
@@ -133,95 +155,151 @@
         ViewCompat.setAccessibilityDelegate(mHost, null);
     }
 
-    private static Rect getBoundsOnScreen(View v) {
-        final int[] tempLocation = new int[2];
-        final Rect hostBoundsOnScreen = new Rect(0, 0, v.getWidth(), v.getHeight());
-        v.getLocationOnScreen(tempLocation);
-        hostBoundsOnScreen.offset(tempLocation[0], tempLocation[1]);
-        return hostBoundsOnScreen;
+    @Test
+    @UiThreadTest
+    public void testMoveFocusDirection() {
+        final ExploreByTouchHelper helper = new TwoNestedViewHelper(mHost);
+        ViewCompat.setAccessibilityDelegate(mHost, helper);
+        helper.requestKeyboardFocusForVirtualView(0);
+
+        boolean moveFocusUp = helper.dispatchKeyEvent(
+                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP));
+        assertEquals(ExploreByTouchHelper.INVALID_ID, helper.getKeyboardFocusedVirtualViewId());
+        assertEquals(false, moveFocusUp);
+
+        boolean moveFocusDown = helper.dispatchKeyEvent(
+                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN));
+        assertEquals(0, helper.getKeyboardFocusedVirtualViewId());
+        assertEquals(true, moveFocusDown);
+
+        ViewCompat.setAccessibilityDelegate(mHost, null);
     }
 
     /**
-     * An extension of ExploreByTouchHelper that contains a single virtual view
-     * whose bounds match the host view.
+     * An extension of ExploreByTouchHelper that contains 2 nested virtual view
+     * and specify {@link AccessibilityNodeInfoCompat#setBoundsInParent}.
      */
-    private static class ParentBoundsHelper extends ExploreByTouchHelper {
-        private final View mHost;
+    private static class ParentBoundsHelper extends TwoNestedViewHelper {
 
         ParentBoundsHelper(View host) {
             super(host);
-
-            mHost = host;
         }
 
         @Override
-        protected int getVirtualViewAt(float x, float y) {
-            return 1;
-        }
-
-        @Override
-        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
-            virtualViewIds.add(1);
-        }
-
-        @Override
-        protected void onPopulateNodeForVirtualView(int virtualViewId,
-                @NonNull AccessibilityNodeInfoCompat node) {
-            if (virtualViewId == 1) {
-                node.setContentDescription("test");
-
-                final Rect hostBounds = new Rect(0, 0, mHost.getWidth(), mHost.getHeight());
-                node.setBoundsInParent(hostBounds);
-            }
-        }
-
-        @Override
-        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
-                Bundle arguments) {
-            return false;
+        protected void onPopulateNodeForVirtualView(
+                int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+            populateNodeForVirtualView(/* setBoundsFromParent= */true,
+                    /* setBoundsFromScreen= */ false, virtualViewId, node);
         }
     }
 
     /**
-     * An extension of ExploreByTouchHelper that contains two virtual views to test moving focus.
+     * An extension of ExploreByTouchHelper that contains 2 nested virtual view
+     * and specify {@link AccessibilityNodeInfoCompat#setBoundsInScreen} by calling
+     * {@link ExploreByTouchHelper#setBoundsInScreenFromBoundsInParent}.
      */
-    private static class FocusTouchHelper extends ExploreByTouchHelper {
-        private final View mHost;
+    private static class ScreenBoundsHelper extends TwoNestedViewHelper {
 
-        FocusTouchHelper(View host) {
+        ScreenBoundsHelper(View host) {
+            super(host);
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(
+                int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+            populateNodeForVirtualView(/* setBoundsFromParent= */false,
+                    /* setBoundsFromScreen= */ true, virtualViewId, node);
+        }
+    }
+
+    /**
+     * An extension of ExploreByTouchHelper that contains 2 nested virtual view
+     * and specify {@link AccessibilityNodeInfoCompat#setBoundsInParent}
+     * and {@link AccessibilityNodeInfoCompat#setBoundsInScreen} by calling
+     * {@link ExploreByTouchHelper#setBoundsInScreenFromBoundsInParent}.
+     */
+    private static class ParentAndScreenBoundsHelper extends TwoNestedViewHelper {
+
+        ParentAndScreenBoundsHelper(View host) {
+            super(host);
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(
+                int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+            populateNodeForVirtualView(/* setBoundsFromParent= */true,
+                    /* setBoundsFromScreen= */ true, virtualViewId, node);
+        }
+    }
+
+    private static class VirtualItem {
+        private int mParentId;
+        private Rect mBoundsInParent;
+        private String mText;
+
+        VirtualItem(int parentId, String text, Rect boundsInParent) {
+            this.mParentId = parentId;
+            this.mBoundsInParent = boundsInParent;
+            this.mText = text;
+        }
+    }
+
+    /**
+     * An extension of ExploreByTouchHelper that contains 2 nested virtual views.
+     * Host view contains 1 child "bottom" and "bottom" contains one child
+     * "nested-bottom-right".
+     */
+    private static class TwoNestedViewHelper extends ExploreByTouchHelper {
+        private final View mHost;
+        protected VirtualItem[] mVirtualItems = new VirtualItem[2];
+
+        TwoNestedViewHelper(View host) {
             super(host);
             mHost = host;
+            mVirtualItems[0] = new VirtualItem(ExploreByTouchHelper.HOST_ID, "bottom",
+                    new Rect(0, mHost.getHeight() / 2,
+                            mHost.getWidth(), mHost.getHeight()));
+            mVirtualItems[1] = new VirtualItem(0, "nested-bottom-right",
+                    new Rect(mHost.getWidth() / 2, 0,
+                            mHost.getWidth(), mHost.getHeight() / 2));
         }
 
         @Override
         protected int getVirtualViewAt(float x, float y) {
-            RectF topHalf = new RectF();
-            topHalf.set(0, 0, mHost.getWidth(), mHost.getHeight() / 2);
-            if (topHalf.contains(x, y)) {
+            if (x < mHost.getWidth() / 2 && y > mHost.getHeight() / 2) {
+                return 0;
+            } else if (x > mHost.getWidth() / 2 && y > mHost.getHeight() / 2) {
                 return 1;
             }
-            return 5;
+            return -1;
         }
 
         @Override
         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
+            virtualViewIds.add(0);
             virtualViewIds.add(1);
-            virtualViewIds.add(5);
         }
 
         @Override
-        protected void onPopulateNodeForVirtualView(int virtualViewId,
+        protected void onPopulateNodeForVirtualView(
+                int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+            populateNodeForVirtualView(/* setBoundsFromParent= */false,
+                    /* setBoundsFromScreen= */ true, virtualViewId, node);
+        }
+
+        protected void populateNodeForVirtualView(boolean setBoundsFromParent,
+                boolean setBoundsFromScreen, int virtualViewId,
                 @NonNull AccessibilityNodeInfoCompat node) {
-            if (virtualViewId == 1) {
-                node.setContentDescription("test 1");
-                final Rect hostBounds = new Rect(0, 0, mHost.getWidth(), mHost.getHeight() / 2);
-                node.setBoundsInParent(hostBounds);
-            }
-            if (virtualViewId == 5) {
-                node.setContentDescription("test 5");
-                final Rect hostBounds =
-                        new Rect(0, mHost.getHeight() / 2, mHost.getWidth(), mHost.getHeight());
-                node.setBoundsInParent(hostBounds);
+            if (virtualViewId <= mVirtualItems.length) {
+                int index = virtualViewId;
+                node.setContentDescription(mVirtualItems[index].mText);
+                node.setParent(mHost, mVirtualItems[index].mParentId);
+                if (setBoundsFromParent) {
+                    node.setBoundsInParent(mVirtualItems[index].mBoundsInParent);
+                }
+                if (setBoundsFromScreen) {
+                    setBoundsInScreenFromBoundsInParent(node, mVirtualItems[index].mBoundsInParent);
+                }
             }
         }
 
@@ -230,6 +308,5 @@
                 Bundle arguments) {
             return false;
         }
-
     }
 }
diff --git a/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java b/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
index ae399dd..adc7915 100644
--- a/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
+++ b/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
@@ -97,7 +97,7 @@
     private static final String DEFAULT_CLASS_NAME = "android.view.View";
 
     /** Default bounds used to determine if the client didn't set any. */
-    private static final Rect INVALID_PARENT_BOUNDS = new Rect(
+    private static final Rect INVALID_BOUNDS = new Rect(
             Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
 
     // Temporary, reusable data structures.
@@ -324,9 +324,9 @@
      * @param virtualViewId the identifier of the virtual view
      * @param outBounds the rect to populate with virtual view bounds
      */
-    private void getBoundsInParent(int virtualViewId, Rect outBounds) {
+    private void getBoundsInScreen(int virtualViewId, Rect outBounds) {
         final AccessibilityNodeInfoCompat node = obtainAccessibilityNodeInfo(virtualViewId);
-        node.getBoundsInParent(outBounds);
+        node.getBoundsInScreen(outBounds);
     }
 
     /**
@@ -336,7 +336,7 @@
             new FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat>() {
                 @Override
                 public void obtainBounds(AccessibilityNodeInfoCompat node, Rect outBounds) {
-                    node.getBoundsInParent(outBounds);
+                    node.getBoundsInScreen(outBounds);
                 }
             };
 
@@ -392,7 +392,7 @@
                 final Rect selectedRect = new Rect();
                 if (mKeyboardFocusedVirtualViewId != INVALID_ID) {
                     // Focus is moving from a virtual view within the host.
-                    getBoundsInParent(mKeyboardFocusedVirtualViewId, selectedRect);
+                    getBoundsInScreen(mKeyboardFocusedVirtualViewId, selectedRect);
                 } else if (previouslyFocusedRect != null) {
                     // Focus is moving from a real view outside the host.
                     selectedRect.set(previouslyFocusedRect);
@@ -772,16 +772,6 @@
      * <li>{@link AccessibilityNodeInfoCompat#setParent(View)}
      * <li>{@link AccessibilityNodeInfoCompat#setSource(View, int)}
      * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
-     * <li>{@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)}
-     * </ul>
-     * <p>
-     * Uses the bounds of the parent view and the parent-relative bounding
-     * rectangle specified by
-     * {@link AccessibilityNodeInfoCompat#getBoundsInParent} to automatically
-     * update the following properties:
-     * <ul>
-     * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
-     * <li>{@link AccessibilityNodeInfoCompat#setBoundsInParent}
      * </ul>
      *
      * @param virtualViewId the virtual view id for item for which to construct
@@ -797,8 +787,8 @@
         node.setFocusable(true);
         node.setClassName(DEFAULT_CLASS_NAME);
 
-        node.setBoundsInParent(INVALID_PARENT_BOUNDS);
-        node.setBoundsInScreen(INVALID_PARENT_BOUNDS);
+        node.setBoundsInParent(INVALID_BOUNDS);
+        node.setBoundsInScreen(INVALID_BOUNDS);
         node.setParent(mHost);
 
         // Allow the client to populate the node.
@@ -811,8 +801,10 @@
         }
 
         node.getBoundsInParent(mTempParentRect);
-        if (mTempParentRect.equals(INVALID_PARENT_BOUNDS)) {
-            throw new RuntimeException("Callbacks must set parent bounds in "
+        node.getBoundsInScreen(mTempScreenRect);
+        if (mTempParentRect.equals(INVALID_BOUNDS) && mTempScreenRect.equals(
+                INVALID_BOUNDS)) {
+            throw new RuntimeException("Callbacks must set parent bounds or screen bounds in "
                     + "populateNodeForVirtualViewId()");
         }
 
@@ -850,32 +842,9 @@
 
         mHost.getLocationOnScreen(mTempGlobalRect);
 
-        // If not explicitly specified, calculate screen-relative bounds and
-        // offset for scroll position based on bounds in parent.
-        node.getBoundsInScreen(mTempScreenRect);
-        if (mTempScreenRect.equals(INVALID_PARENT_BOUNDS)) {
-            node.getBoundsInParent(mTempScreenRect);
-
-            // If there is a parent node, adjust bounds based on the parent node.
-            if (node.mParentVirtualDescendantId != HOST_ID) {
-                AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
-                // Walk up the node tree to adjust the screen rect.
-                for (int virtualDescendantId = node.mParentVirtualDescendantId;
-                        virtualDescendantId != HOST_ID;
-                        virtualDescendantId = parentNode.mParentVirtualDescendantId) {
-                    // Reset the values in the parent node we'll be using.
-                    parentNode.setParent(mHost, HOST_ID);
-                    parentNode.setBoundsInParent(INVALID_PARENT_BOUNDS);
-                    // Adjust the bounds for the parent node.
-                    onPopulateNodeForVirtualView(virtualDescendantId, parentNode);
-                    parentNode.getBoundsInParent(mTempParentRect);
-                    mTempScreenRect.offset(mTempParentRect.left, mTempParentRect.top);
-                }
-                parentNode.recycle();
-            }
-            // Adjust the rect for the host view's location.
-            mTempScreenRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
-                    mTempGlobalRect[1] - mHost.getScrollY());
+        if (mTempScreenRect.equals(INVALID_BOUNDS)) {
+            setBoundsInScreenFromBoundsInParent(node, mTempParentRect);
+            node.getBoundsInScreen(mTempScreenRect);
         }
 
         if (mHost.getLocalVisibleRect(mTempVisibleRect)) {
@@ -884,7 +853,6 @@
             final boolean intersects = mTempScreenRect.intersect(mTempVisibleRect);
             if (intersects) {
                 node.setBoundsInScreen(mTempScreenRect);
-
                 if (isVisibleToUser(mTempScreenRect)) {
                     node.setVisibleToUser(true);
                 }
@@ -924,15 +892,15 @@
 
     /**
      * Computes whether the specified {@link Rect} intersects with the visible
-     * portion of its parent {@link View}. Modifies {@code localRect} to contain
+     * portion of its parent {@link View}. Modifies {@code screenRect} to contain
      * only the visible portion.
      *
-     * @param localRect a rectangle in local (parent) coordinates
+     * @param screenRect a rectangle in screen coordinates
      * @return whether the specified {@link Rect} is visible on the screen
      */
-    private boolean isVisibleToUser(Rect localRect) {
+    private boolean isVisibleToUser(Rect screenRect) {
         // Missing or empty bounds mean this view is not visible.
-        if ((localRect == null) || localRect.isEmpty()) {
+        if ((screenRect == null) || screenRect.isEmpty()) {
             return false;
         }
 
@@ -1064,6 +1032,46 @@
     }
 
     /**
+     * Calculates and assigns screen-relative bounds based on bounds in parent. Instead
+     * of calling the deprecated {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}, it
+     * provides a convenient method to calculate and assign
+     * {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} based on {@code boundsInParent}.
+     *
+     * @param node The node to populate
+     * @param boundsInParent The node bounds in the viewParent's coordinates.
+     */
+    public final void setBoundsInScreenFromBoundsInParent(@NonNull AccessibilityNodeInfoCompat node,
+            @NonNull Rect boundsInParent) {
+        node.setBoundsInParent(boundsInParent);
+        Rect screenRect = new Rect();
+        screenRect.set(boundsInParent);
+
+        // If there is a parent node, adjust bounds based on the parent node.
+        if (node.mParentVirtualDescendantId != HOST_ID) {
+            AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
+            Rect tempParentRect = new Rect();
+            // Walk up the node tree to adjust the screen rect.
+            for (int virtualDescendantId = node.mParentVirtualDescendantId;
+                    virtualDescendantId != HOST_ID;
+                    virtualDescendantId = parentNode.mParentVirtualDescendantId) {
+                // Reset the values in the parent node we'll be using.
+                parentNode.setParent(mHost, HOST_ID);
+                parentNode.setBoundsInParent(INVALID_BOUNDS);
+                // Adjust the bounds for the parent node.
+                onPopulateNodeForVirtualView(virtualDescendantId, parentNode);
+                parentNode.getBoundsInParent(tempParentRect);
+                screenRect.offset(tempParentRect.left, tempParentRect.top);
+            }
+            parentNode.recycle();
+        }
+        // Adjust the rect for the host view's location.
+        mHost.getLocationOnScreen(mTempGlobalRect);
+        screenRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
+                mTempGlobalRect[1] - mHost.getScrollY());
+        node.setBoundsInScreen(screenRect);
+    }
+
+    /**
      * Provides a mapping between view-relative coordinates and logical
      * items.
      *
@@ -1144,8 +1152,10 @@
      * <li>event text, see
      * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or
      * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)}
-     * <li>bounds in parent coordinates, see
-     * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}
+     * <li>bounds in screen coordinates, see
+     * {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} and
+     * {@link ExploreByTouchHelper#setBoundsInScreenFromBoundsInParent
+     * (AccessibilityNodeInfoCompat, Rect)}
      * </ul>
      * <p>
      * The helper class automatically populates the following fields with
@@ -1176,8 +1186,6 @@
      * {@link AccessibilityNodeInfoCompat#setAccessibilityFocused(boolean)}
      * <li>keyboard focus, computed based on internal helper state, see
      * {@link AccessibilityNodeInfoCompat#setFocused(boolean)}
-     * <li>bounds in screen coordinates, computed based on host view bounds,
-     * see {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)}
      * </ul>
      * <p>
      * Additionally, the helper class automatically handles keyboard focus and
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt
index c858653d..4318b91 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt
@@ -35,6 +35,9 @@
      *
      * Note that this will always be called before each call to [migrate].
      *
+     * Note that accessing any data from DataStore directly from inside this function will result
+     * in deadlock, since DataStore doesn't return data until all migrations complete.
+     *
      * @param currentData the current data (which might already populated from previous runs of this
      * or other migrations)
      */
@@ -49,6 +52,9 @@
      *
      * Note that this will always be called before a call to [cleanUp].
      *
+     * Note that accessing any data from DataStore directly from inside this function will result
+     * in deadlock, since DataStore doesn't return data until all migrations complete.
+     *
      * @param currentData the current data (it might be populated from other migrations or from
      * manual changes before this migration was added to the app)
      * @return The migrated data.
@@ -61,6 +67,10 @@
      * back to the DataStore call that triggered the migration and future calls to DataStore will
      * result in DataMigrations being attempted again. This method may be run multiple times when
      * any failure is encountered.
+     *
+     * This is useful for cleaning up files or data outside of DataStore and accessing any
+     * data from DataStore directly from inside this function will result in deadlock, since
+     * DataStore doesn't return data until all migrations complete.
      */
     public suspend fun cleanUp()
 }
\ No newline at end of file
diff --git a/datastore/datastore-rxjava2/api/current.txt b/datastore/datastore-rxjava2/api/current.txt
index 42e32ea..b52a1fc 100644
--- a/datastore/datastore-rxjava2/api/current.txt
+++ b/datastore/datastore-rxjava2/api/current.txt
@@ -1,20 +1,37 @@
 // Signature format: 4.0
 package androidx.datastore.rxjava2 {
 
+  public interface RxDataMigration<T> {
+    method public io.reactivex.Completable cleanUp();
+    method public io.reactivex.Single<T!> migrate(T?);
+    method public io.reactivex.Single<java.lang.Boolean!> shouldMigrate(T?);
+  }
+
   public final class RxDataStore {
     method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
     method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
   }
 
   public final class RxDataStoreBuilder<T> {
-    ctor public RxDataStoreBuilder();
+    ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+    ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<T> rxDataMigration);
     method public androidx.datastore.core.DataStore<T> build();
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileName(android.content.Context context, String fileName);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileProducer(java.util.concurrent.Callable<java.io.File> produceFile);
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.Scheduler ioScheduler);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setSerializer(androidx.datastore.core.Serializer<T> serializer);
+  }
+
+  public interface RxSharedPreferencesMigration<T> {
+    method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+    method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+  }
+
+  public final class RxSharedPreferencesMigrationBuilder<T> {
+    ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+    method public androidx.datastore.core.DataMigration<T> build();
+    method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+    method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
   }
 
 }
diff --git a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
index 42e32ea..b52a1fc 100644
--- a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
@@ -1,20 +1,37 @@
 // Signature format: 4.0
 package androidx.datastore.rxjava2 {
 
+  public interface RxDataMigration<T> {
+    method public io.reactivex.Completable cleanUp();
+    method public io.reactivex.Single<T!> migrate(T?);
+    method public io.reactivex.Single<java.lang.Boolean!> shouldMigrate(T?);
+  }
+
   public final class RxDataStore {
     method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
     method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
   }
 
   public final class RxDataStoreBuilder<T> {
-    ctor public RxDataStoreBuilder();
+    ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+    ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<T> rxDataMigration);
     method public androidx.datastore.core.DataStore<T> build();
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileName(android.content.Context context, String fileName);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileProducer(java.util.concurrent.Callable<java.io.File> produceFile);
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.Scheduler ioScheduler);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setSerializer(androidx.datastore.core.Serializer<T> serializer);
+  }
+
+  public interface RxSharedPreferencesMigration<T> {
+    method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+    method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+  }
+
+  public final class RxSharedPreferencesMigrationBuilder<T> {
+    ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+    method public androidx.datastore.core.DataMigration<T> build();
+    method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+    method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
   }
 
 }
diff --git a/datastore/datastore-rxjava2/api/restricted_current.txt b/datastore/datastore-rxjava2/api/restricted_current.txt
index 42e32ea..b52a1fc 100644
--- a/datastore/datastore-rxjava2/api/restricted_current.txt
+++ b/datastore/datastore-rxjava2/api/restricted_current.txt
@@ -1,20 +1,37 @@
 // Signature format: 4.0
 package androidx.datastore.rxjava2 {
 
+  public interface RxDataMigration<T> {
+    method public io.reactivex.Completable cleanUp();
+    method public io.reactivex.Single<T!> migrate(T?);
+    method public io.reactivex.Single<java.lang.Boolean!> shouldMigrate(T?);
+  }
+
   public final class RxDataStore {
     method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
     method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
   }
 
   public final class RxDataStoreBuilder<T> {
-    ctor public RxDataStoreBuilder();
+    ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+    ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<T> rxDataMigration);
     method public androidx.datastore.core.DataStore<T> build();
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileName(android.content.Context context, String fileName);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileProducer(java.util.concurrent.Callable<java.io.File> produceFile);
     method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.Scheduler ioScheduler);
-    method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setSerializer(androidx.datastore.core.Serializer<T> serializer);
+  }
+
+  public interface RxSharedPreferencesMigration<T> {
+    method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+    method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+  }
+
+  public final class RxSharedPreferencesMigrationBuilder<T> {
+    ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+    method public androidx.datastore.core.DataMigration<T> build();
+    method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+    method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
   }
 
 }
diff --git a/datastore/datastore-rxjava2/build.gradle b/datastore/datastore-rxjava2/build.gradle
index 6cf3913..83003c2 100644
--- a/datastore/datastore-rxjava2/build.gradle
+++ b/datastore/datastore-rxjava2/build.gradle
@@ -27,19 +27,12 @@
 }
 
 android {
-    buildTypes{
-        debug {
-            multiDexEnabled = true
-        }
-    }
-
     sourceSets {
         test.java.srcDirs += 'src/test-common/java'
         androidTest.java.srcDirs += 'src/test-common/java'
     }
 }
 
-
 dependencies {
     api(KOTLIN_STDLIB)
     api(KOTLIN_COROUTINES_CORE)
@@ -55,11 +48,7 @@
     testImplementation(TRUTH)
     testImplementation(project(":internal-testutils-truth"))
 
-    androidTestImplementation(project(":datastore:datastore-core"))
-    androidTestImplementation(project(":datastore:datastore"))
     androidTestImplementation(JUNIT)
-    androidTestImplementation(KOTLIN_COROUTINES_TEST)
-    androidTestImplementation(TRUTH)
     androidTestImplementation(project(":internal-testutils-truth"))
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ANDROIDX_TEST_CORE)
@@ -73,10 +62,3 @@
     description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
     legacyDisableKotlinStrictApiMode = true
 }
-
-// Allow usage of Kotlin's @OptIn.
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
-    }
-}
\ No newline at end of file
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/datastore/datastore-rxjava2/src/androidTest/AndroidManifest.xml
similarity index 94%
rename from car/app/app/src/androidTest/AndroidManifest.xml
rename to datastore/datastore-rxjava2/src/androidTest/AndroidManifest.xml
index 3bc2684..bf9d5c2 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/datastore/datastore-rxjava2/src/androidTest/AndroidManifest.xml
@@ -15,5 +15,6 @@
   limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.car.app">
+    package="androidx.datastore.rxjava2">
+
 </manifest>
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
index f734adc..cc1f3493 100644
--- a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.datastore.rxjava2
 
 @Suppress("UNCHECKED_CAST")
@@ -35,4 +34,4 @@
             expectedType.simpleName
         )
     )
-}
\ No newline at end of file
+}
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java
index 6eceb67..2d3a0ae 100644
--- a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.datastore.rxjava2;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
 
+import androidx.annotation.NonNull;
 import androidx.datastore.core.DataStore;
 import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
 import androidx.test.core.app.ApplicationProvider;
@@ -32,6 +32,7 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 
+import io.reactivex.Completable;
 import io.reactivex.Scheduler;
 import io.reactivex.Single;
 import io.reactivex.schedulers.Schedulers;
@@ -47,60 +48,68 @@
     @Test
     public void testConstructWithProduceFile() throws Exception {
         File file = tempFolder.newFile();
-
         DataStore<Byte> dataStore =
-                new RxDataStoreBuilder<Byte>()
-                        .setFileProducer(() -> file)
-                        .setSerializer(new TestingSerializer())
+                new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
                         .build();
-
         Single<Byte> incrementByte = RxDataStore.updateDataAsync(dataStore,
                 RxDataStoreBuilderTest::incrementByte);
         assertThat(incrementByte.blockingGet()).isEqualTo(1);
-
         // Construct it again and confirm that the data is still there:
         dataStore =
-                new RxDataStoreBuilder<Byte>()
-                        .setFileProducer(() -> file)
-                        .setSerializer(new TestingSerializer())
+                new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
                         .build();
-
         assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
     }
 
     @Test
     public void testConstructWithContextAndName() throws Exception {
-
         Context context = ApplicationProvider.getApplicationContext();
         String name = "my_data_store";
-
         DataStore<Byte> dataStore =
-                new RxDataStoreBuilder<Byte>()
-                        .setFileName(context, name)
-                        .setSerializer(new TestingSerializer())
+                new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
                         .build();
-
         Single<Byte> set1 = RxDataStore.updateDataAsync(dataStore, input -> Single.just((byte) 1));
         assertThat(set1.blockingGet()).isEqualTo(1);
-
         // Construct it again and confirm that the data is still there:
         dataStore =
-                new RxDataStoreBuilder<Byte>()
-                        .setFileName(context, name)
-                        .setSerializer(new TestingSerializer())
+                new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
                         .build();
-
         assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
-
         // Construct it again with the expected file path and confirm that the data is there:
         dataStore =
-                new RxDataStoreBuilder<Byte>()
-                        .setFileProducer(
-                                () -> new File(
-                                        context.getFilesDir().getPath()
-                                                + "/datastore/" + name))
-                        .setSerializer(new TestingSerializer())
+                new RxDataStoreBuilder<Byte>(() -> new File(context.getFilesDir().getPath()
+                        + "/datastore/" + name), new TestingSerializer()
+                )
                         .build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+    }
+
+    @Test
+    public void testMigrationsAreInstalledAndRun() throws Exception {
+        RxDataMigration<Byte> plusOneMigration = new RxDataMigration<Byte>() {
+            @NonNull
+            @Override
+            public Single<Boolean> shouldMigrate(@NonNull Byte currentData) {
+                return Single.just(true);
+            }
+
+            @NonNull
+            @Override
+            public Single<Byte> migrate(@NonNull Byte currentData) {
+                return incrementByte(currentData);
+            }
+
+            @NonNull
+            @Override
+            public Completable cleanUp() {
+                return Completable.complete();
+            }
+        };
+
+        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+                () -> tempFolder.newFile(), new TestingSerializer())
+                .addRxDataMigration(plusOneMigration)
+                .build();
 
         assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
     }
@@ -112,82 +121,41 @@
                     @Override
                     public Thread newThread(Runnable r) {
                         return new Thread(r, "TestingThread");
-
                     }
                 }));
 
 
-        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>()
-                .setFileProducer(() -> tempFolder.newFile())
-                .setSerializer(new TestingSerializer())
+        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(() -> tempFolder.newFile(),
+                new TestingSerializer())
                 .setIoScheduler(singleThreadedScheduler)
                 .build();
-
         Single<Byte> update = RxDataStore.updateDataAsync(dataStore, input -> {
             Thread currentThread = Thread.currentThread();
             assertThat(currentThread.getName()).isEqualTo("TestingThread");
-
             return Single.just(input);
         });
-
         assertThat(update.blockingGet()).isEqualTo((byte) 0);
-
         Single<Byte> subsequentUpdate = RxDataStore.updateDataAsync(dataStore, input -> {
             Thread currentThread = Thread.currentThread();
             assertThat(currentThread.getName()).isEqualTo("TestingThread");
-
             return Single.just(input);
         });
-
         assertThat(subsequentUpdate.blockingGet()).isEqualTo((byte) 0);
-
     }
 
     @Test
     public void testCorruptionHandlerIsUser() {
         TestingSerializer testingSerializer = new TestingSerializer();
         testingSerializer.setFailReadWithCorruptionException(true);
-
         ReplaceFileCorruptionHandler<Byte> replaceFileCorruptionHandler =
-                 new ReplaceFileCorruptionHandler<Byte>(exception -> (byte) 99);
+                new ReplaceFileCorruptionHandler<Byte>(exception -> (byte) 99);
 
 
-        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>()
-                .setFileProducer(() -> tempFolder.newFile())
-                .setSerializer(testingSerializer)
+        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+                () -> tempFolder.newFile(),
+                testingSerializer)
                 .setCorruptionHandler(replaceFileCorruptionHandler)
                 .build();
-
         assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(99);
     }
-
-    @Test
-    public void testSerializerIsRequired() {
-        AssertThrowsKt.assertThrows(IllegalStateException.class,
-                () -> new RxDataStoreBuilder<Byte>().setFileProducer(
-                        () -> tempFolder.newFile()).build());
-    }
-
-    @Test
-    public void testOneOfProduceFileOrContextAndNameIsRequired() {
-        // Set file producer then context and name
-        AssertThrowsKt.assertThrows(IllegalStateException.class,
-                () -> new RxDataStoreBuilder<Byte>()
-                        .setSerializer(new TestingSerializer())
-                        .setFileProducer(() -> tempFolder.newFile())
-                        .setFileName(ApplicationProvider.getApplicationContext(), "name"));
-
-        // Set context and name then file producer
-        AssertThrowsKt.assertThrows(IllegalStateException.class,
-                () -> new RxDataStoreBuilder<Byte>()
-                        .setSerializer(new TestingSerializer())
-                        .setFileName(ApplicationProvider.getApplicationContext(), "name")
-                        .setFileProducer(() -> tempFolder.newFile()));
-
-        // Set neither and try to build.
-        AssertThrowsKt.assertThrows(IllegalStateException.class,
-                () -> new RxDataStoreBuilder<Byte>()
-                        .setSerializer(new TestingSerializer())
-                        .build());
-    }
 }
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java
new file mode 100644
index 0000000..c8e0b79
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2;
+
+import static androidx.testutils.AssertionsKt.assertThrows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.datastore.core.DataMigration;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.migrations.SharedPreferencesView;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+import io.reactivex.Single;
+
+public class RxSharedPreferencesMigrationTest {
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private final String mSharedPrefsName = "shared_prefs_name";
+
+
+    private Context mContext;
+    private SharedPreferences mSharedPrefs;
+    private File mDatastoreFile;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mSharedPrefs = mContext.getSharedPreferences(mSharedPrefsName, Context.MODE_PRIVATE);
+        mDatastoreFile = temporaryFolder.newFile("test_file.preferences_pb");
+
+        assertThat(mSharedPrefs.edit().clear().commit()).isTrue();
+    }
+
+    @Test
+    public void testShouldMigrateSkipsMigration() {
+        RxSharedPreferencesMigration<Byte> skippedMigration =
+                new RxSharedPreferencesMigration<Byte>() {
+                    @NotNull
+                    @Override
+                    public Single<Boolean> shouldMigrate(Byte currentData) {
+                        return Single.just(false);
+                    }
+
+                    @NotNull
+                    @Override
+                    public Single<Byte> migrate(
+                            @NotNull SharedPreferencesView sharedPreferencesView,
+                            Byte currentData) {
+                        return Single.error(
+                                new IllegalStateException("We shouldn't reach this point!"));
+                    }
+                };
+
+
+        DataMigration<Byte> spMigration =
+                getSpMigrationBuilder(skippedMigration).build();
+
+        DataStore<Byte> dataStoreWithMigrations = getDataStoreWithMigration(spMigration);
+
+        assertThat(RxDataStore.data(dataStoreWithMigrations).blockingFirst()).isEqualTo(0);
+    }
+
+    @Test
+    public void testSharedPrefsViewContainsSpecifiedKeys() {
+        String includedKey = "key1";
+        int includedVal = 99;
+        String notMigratedKey = "key2";
+
+        assertThat(mSharedPrefs.edit().putInt(includedKey, includedVal).putInt(notMigratedKey,
+                123).commit()).isTrue();
+
+        DataMigration<Byte> dataMigration =
+                getSpMigrationBuilder(
+                        new DefaultMigration() {
+                            @NotNull
+                            @Override
+                            public Single<Byte> migrate(
+                                    @NotNull SharedPreferencesView sharedPreferencesView,
+                                    Byte currentData) {
+                                assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+                                assertThat(sharedPreferencesView.getAll().size()).isEqualTo(1);
+                                assertThrows(IllegalStateException.class,
+                                        () -> sharedPreferencesView.getInt(notMigratedKey, -1));
+
+                                return Single.just((byte) 50);
+                            }
+                        }
+                ).setKeysToMigrate(includedKey).build();
+
+        DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+        assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+        assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+        assertThat(mSharedPrefs.contains(notMigratedKey)).isTrue();
+    }
+
+
+    @Test
+    public void testSharedPrefsViewWithAllKeysSpecified() {
+        String includedKey = "key1";
+        String includedKey2 = "key2";
+        int value = 99;
+
+        assertThat(mSharedPrefs.edit().putInt(includedKey, value).putInt(includedKey2,
+                value).commit()).isTrue();
+
+        DataMigration<Byte> dataMigration =
+                getSpMigrationBuilder(
+                        new DefaultMigration() {
+                            @NotNull
+                            @Override
+                            public Single<Byte> migrate(
+                                    @NotNull SharedPreferencesView sharedPreferencesView,
+                                    Byte currentData) {
+                                assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+                                assertThat(sharedPreferencesView.contains(includedKey2)).isTrue();
+                                assertThat(sharedPreferencesView.getAll().size()).isEqualTo(2);
+
+                                return Single.just((byte) 50);
+                            }
+                        }
+                ).build();
+
+        DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+        assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+        assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+        assertThat(mSharedPrefs.contains(includedKey2)).isFalse();
+    }
+
+    @Test
+    public void testDeletesEmptySharedPreferences() {
+        String key = "key";
+        String value = "value";
+        assertThat(mSharedPrefs.edit().putString(key, value).commit()).isTrue();
+
+        DataMigration<Byte> dataMigration =
+                getSpMigrationBuilder(new DefaultMigration()).setDeleteEmptyPreferences(
+                        true).build();
+        DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+        assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(0);
+
+        // Check that the shared preferences files are deleted
+        File prefsDir = new File(mContext.getApplicationInfo().dataDir, "shared_prefs");
+        File prefsFile = new File(prefsDir, mSharedPrefsName + ".xml");
+        File backupPrefsFile = new File(prefsFile.getPath() + ".bak");
+        assertThat(prefsFile.exists()).isFalse();
+        assertThat(backupPrefsFile.exists()).isFalse();
+    }
+
+    private RxSharedPreferencesMigrationBuilder<Byte> getSpMigrationBuilder(
+            RxSharedPreferencesMigration<Byte> rxSharedPreferencesMigration) {
+        return new RxSharedPreferencesMigrationBuilder<Byte>(mContext, mSharedPrefsName,
+                rxSharedPreferencesMigration);
+    }
+
+    private DataStore<Byte> getDataStoreWithMigration(DataMigration<Byte> dataMigration) {
+        return new RxDataStoreBuilder<Byte>(() -> mDatastoreFile, new TestingSerializer())
+                .addDataMigration(dataMigration).build();
+    }
+
+
+    private static class DefaultMigration implements RxSharedPreferencesMigration<Byte> {
+
+        @NotNull
+        @Override
+        public Single<Boolean> shouldMigrate(Byte currentData) {
+            return Single.just(true);
+        }
+
+        @NotNull
+        @Override
+        public Single<Byte> migrate(@NotNull SharedPreferencesView sharedPreferencesView,
+                Byte currentData) {
+            return Single.just(currentData);
+        }
+    }
+}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataMigration.java b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataMigration.java
new file mode 100644
index 0000000..79df6a1
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataMigration.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.reactivex.Completable;
+import io.reactivex.Single;
+
+/**
+ * Interface for migrations to DataStore. Methods on this migration ([shouldMigrate], [migrate]
+ * and [cleanUp]) may be called multiple times, so their implementations must be idempotent.
+ * These methods may be called multiple times if DataStore encounters issues when writing the
+ * newly migrated data to disk or if any migration installed in the same DataStore throws an
+ * Exception.
+ *
+ * If you're migrating from SharedPreferences see [SharedPreferencesMigration].
+ *
+ * @param <T> the exception type
+ */
+public interface RxDataMigration<T> {
+
+    /**
+     * Return whether this migration needs to be performed. If this returns false, no migration or
+     * cleanup will occur. Apps should do the cheapest possible check to determine if this migration
+     * should run, since this will be called every time the DataStore is initialized. This method
+     * may be run multiple times when any failure is encountered.
+     *
+     * Note that this will always be called before each call to [migrate].
+     *
+     * @param currentData the current data (which might already populated from previous runs of this
+     *                    or other migrations). Only Nullable if the type used with DataStore is
+     *                    Nullable.
+     */
+    @NonNull
+    Single<Boolean> shouldMigrate(@Nullable T currentData);
+
+    /**
+     * Perform the migration. Implementations should be idempotent since this may be called
+     * multiple times. If migrate fails, DataStore will not commit any data to disk, cleanUp will
+     * not be called, and the exception will be propagated back to the DataStore call that
+     * triggered the migration. Future calls to DataStore will result in DataMigrations being
+     * attempted again. This method may be run multiple times when any failure is encountered.
+     *
+     * Note that this will always be called before a call to [cleanUp].
+     *
+     * @param currentData the current data (it might be populated from other migrations or from
+     *                    manual changes before this migration was added to the app). Only
+     *                    Nullable if the type used with DataStore is Nullable.
+     * @return The migrated data.
+     */
+    @NonNull
+    Single<T> migrate(@Nullable T currentData);
+
+    /**
+     * Clean up any old state/data that was migrated into the DataStore. This will not be called
+     * if the migration fails. If cleanUp throws an exception, the exception will be propagated
+     * back to the DataStore call that triggered the migration and future calls to DataStore will
+     * result in DataMigrations being attempted again. This method may be run multiple times when
+     * any failure is encountered.
+     */
+    @NonNull
+    Completable cleanUp();
+}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt
index 1685286..070d0a1c 100644
--- a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt
@@ -28,6 +28,7 @@
 import io.reactivex.schedulers.Schedulers
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.rx2.asCoroutineDispatcher
+import kotlinx.coroutines.rx2.await
 import java.io.File
 import java.util.concurrent.Callable
 
@@ -35,15 +36,49 @@
  * RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
  */
 @SuppressLint("TopLevelBuilder")
-public class RxDataStoreBuilder<T>() {
+public class RxDataStoreBuilder<T> {
 
-    // Either produceFile or context & name must be set, but not both.
+    /**
+     * Create a RxDataStoreBuilder with the callable which returns the File that DataStore acts on.
+     * The user is responsible for ensuring that there is never more than one DataStore acting on
+     * a file at a time.
+     *
+     * @param produceFile Function which returns the file that the new DataStore will act on. The
+     * function must return the same path every time. No two instances of DataStore should act on
+     * the same file at the same time.
+     * @param serializer the serializer for the type that this DataStore acts on.
+     */
+    public constructor(produceFile: Callable<File>, serializer: Serializer<T>) {
+        this.produceFile = produceFile
+        this.serializer = serializer
+    }
+
+    /**
+     * Create a RxDataStoreBuilder with the Context and name from which to derive the DataStore
+     * file. The file is generated by See [Context.createDataStore] for more info. The user is
+     * responsible for ensuring that there is never more than one DataStore acting on a file at a
+     * time.
+     *
+     * @param context the context from which we retrieve files directory.
+     * @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
+     * obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
+     * act on the same file at the same time.
+     * @param serializer the serializer for the type that this DataStore acts on.
+     */
+    public constructor(context: Context, fileName: String, serializer: Serializer<T>) {
+        this.context = context
+        this.name = fileName
+        this.serializer = serializer
+    }
+
+    // Either produceFile or context & name must be set, but not both. This is enforced by the
+    // two constructors.
     private var produceFile: Callable<File>? = null
 
     private var context: Context? = null
     private var name: String? = null
 
-    // Required
+    // Required. This is enforced by the constructors.
     private var serializer: Serializer<T>? = null
 
     // Optional
@@ -52,61 +87,6 @@
     private val dataMigrations: MutableList<DataMigration<T>> = mutableListOf()
 
     /**
-     * Set the callable which returns the File that DataStore acts on. The user is responsible for
-     * ensuring that there is never more than one DataStore acting on a file at a time.
-     *
-     * It is required to call either this method or [setFileName] before calling [build].
-     *
-     *
-     * @param produceFile Function which returns the file that the new DataStore will act on. The
-     * function must return the same path every time. No two instances of DataStore should act on
-     * the same file at the same time.
-     * @throws IllegalStateException if context and name are already set
-     * @return this
-     */
-    @Suppress("MissingGetterMatchingBuilder")
-    public fun setFileProducer(produceFile: Callable<File>): RxDataStoreBuilder<T> = apply {
-        check(context == null) { "Only call setFileProducer or setContextAndName" }
-        check(name == null) { "Only call setFileProducer or setContextAndName" }
-        this.produceFile = produceFile
-    }
-
-    /**
-     * Set the Context and name from which to derive the DataStore file. The file is generated by
-     * See [Context.createDataStore] for more info. The user is responsible for ensuring that
-     * there is never more than one DataStore acting on a file at a time.
-     *
-     * It is required to call either this method or [setFileProducer] before calling [build].
-     *
-     * @param context the context from which we retrieve files directory.
-     * @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
-     * obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
-     * act on the same file at the same time.
-     * @throws IllegalStateException if produceFile is already set
-     * @return this
-     */
-    @Suppress("MissingGetterMatchingBuilder")
-    public fun setFileName(context: Context, fileName: String): RxDataStoreBuilder<T> =
-        apply {
-            check(produceFile == null) { "Only call setFileProducer or setContextAndName" }
-            this.context = context
-            this.name = fileName
-        }
-
-    /**
-     * Set the serializer that this DataStore acts on.
-     *
-     * This parameter is required.
-     *
-     * @param serializer the serializer for the type that this DataStore acts on.
-     * @return this
-     */
-    @Suppress("MissingGetterMatchingBuilder")
-    public fun setSerializer(serializer: Serializer<T>): RxDataStoreBuilder<T> = apply {
-        this.serializer = serializer
-    }
-
-    /**
      * Set the Scheduler on which to perform IO and transform operations. This is converted into
      * a CoroutineDispatcher before being added to DataStore.
      *
@@ -132,6 +112,18 @@
         RxDataStoreBuilder<T> = apply { this.corruptionHandler = corruptionHandler }
 
     /**
+     * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+     *
+     * @param rxDataMigration the migration to add.
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun addRxDataMigration(rxDataMigration: RxDataMigration<T>): RxDataStoreBuilder<T> =
+        apply {
+            this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+        }
+
+    /**
      * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
      *
      * @param dataMigration the migration to add
@@ -145,15 +137,9 @@
     /**
      * Build the DataStore.
      *
-     * @throws IllegalStateException if serializer is not set or if neither produceFile not
-     * context and name are set.
      * @return the DataStore with the provided parameters
      */
     public fun build(): DataStore<T> {
-        check(serializer != null) {
-            "Serializer must be set."
-        }
-
         val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
 
         return if (produceFile != null) {
@@ -175,7 +161,24 @@
                 migrations = dataMigrations
             )
         } else {
-            throw IllegalStateException("Either produceFile or context and name must be set.")
+            error(
+                "Either produceFile or context and name must be set. This should never happen."
+            )
         }
     }
 }
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+    DataMigration<T> {
+    override suspend fun shouldMigrate(currentData: T): Boolean {
+        return migration.shouldMigrate(currentData).await()
+    }
+
+    override suspend fun migrate(currentData: T): T {
+        return migration.migrate(currentData).await()
+    }
+
+    override suspend fun cleanUp() {
+        migration.cleanUp().await()
+    }
+}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
new file mode 100644
index 0000000..e84e377
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.migrations.SharedPreferencesMigration
+import androidx.datastore.migrations.SharedPreferencesView
+import io.reactivex.Single
+import kotlinx.coroutines.rx2.await
+
+/**
+ * Client implemented migration interface.
+ **/
+public interface RxSharedPreferencesMigration<T> {
+    /**
+     * Whether or not the migration should be run. This can be used to skip a read from the
+     * SharedPreferences.
+     *
+     * @param currentData the most recently persisted data
+     * @return a Single indicating whether or not the migration should be run.
+     */
+    public fun shouldMigrate(currentData: T): Single<Boolean> {
+        return Single.just(true)
+    }
+
+    /**
+     * Maps SharedPreferences into T. Implementations should be idempotent
+     * since this may be called multiple times. See [DataMigration.migrate] for more
+     * information. The method accepts a SharedPreferencesView which is the view of the
+     * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
+     * the current data. The function must return the migrated data.
+     *
+     * @param sharedPreferencesView the current state of the SharedPreferences
+     * @param currentData the most recently persisted data
+     * @return a Single of the updated data
+     */
+    public fun migrate(sharedPreferencesView: SharedPreferencesView, currentData: T): Single<T>
+}
+
+/**
+ * RxSharedPreferencesMigrationBuilder for the RxSharedPreferencesMigration.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxSharedPreferencesMigrationBuilder<T>
+/**
+ * Construct a RxSharedPreferencesMigrationBuilder.
+ *
+ * @param context the Context used for getting the SharedPreferences.
+ * @param sharedPreferencesName the name of the SharedPreference from which to migrate.
+ * @param rxSharedPreferencesMigration the user implemented migration for this SharedPreference.
+ */
+constructor(
+    private val context: Context,
+    private val sharedPreferencesName: String,
+    private val rxSharedPreferencesMigration: RxSharedPreferencesMigration<T>
+) {
+
+    /** Optional */
+    private var deleteEmptyPreference: Boolean = true
+    private var keysToMigrate: Set<String>? = null
+
+    /**
+     * Set the list of keys to migrate. The keys will be mapped to datastore.Preferences with
+     * their same values. If the key is already present in the new Preferences, the key
+     * will not be migrated again. If the key is not present in the SharedPreferences it
+     * will not be migrated.
+     *
+     * This method is optional and if keysToMigrate is not set, all keys will be migrated from the
+     * existing SharedPreferences.
+     *
+     * @param keys the keys to migrate
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setKeysToMigrate(vararg keys: String):
+        RxSharedPreferencesMigrationBuilder<T> = apply {
+            keysToMigrate = setOf(*keys)
+        }
+
+    /**
+     * If enabled and the SharedPreferences are empty (i.e. no remaining
+     * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
+     * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
+     * SharedPreferences to begin with then the (potentially) empty SharedPreferences
+     * won't be cleaned up by this option. This functionality is best effort - if there
+     * is an issue deleting the SharedPreferences file it will be silently ignored.
+     *
+     * This method is optional and defaults to true.
+     *
+     * @param deleteEmptyPreferences whether or not to delete the empty shared preferences file
+     * @return this
+     */
+    @Suppress("MissingGetterMatchingBuilder")
+    public fun setDeleteEmptyPreferences(deleteEmptyPreferences: Boolean):
+        RxSharedPreferencesMigrationBuilder<T> = apply {
+            this.deleteEmptyPreference = deleteEmptyPreferences
+        }
+
+    public fun build(): DataMigration<T> {
+        return SharedPreferencesMigration(
+            context = context,
+            sharedPreferencesName = sharedPreferencesName,
+            migrate = { spView, curData ->
+                rxSharedPreferencesMigration.migrate(spView, curData).await()
+            },
+            keysToMigrate = keysToMigrate,
+            deleteEmptyPreferences = deleteEmptyPreference,
+            shouldRunMigration = { curData ->
+                rxSharedPreferencesMigration.shouldMigrate(curData).await()
+            }
+        )
+    }
+}
diff --git a/development/build_log_processor.sh b/development/build_log_processor.sh
index 9802bf4..15a6082 100755
--- a/development/build_log_processor.sh
+++ b/development/build_log_processor.sh
@@ -86,8 +86,6 @@
     if $SCRIPT_PATH/build_log_simplifier.py --validate $logFile >&2; then
       echo No unrecognized messages found in build log
     else
-      echo >&2
-      echo "Build log validation, enabled by the argument $validateArgument, failed" >&2
       exit 1
     fi
   fi
diff --git a/development/build_log_simplifier/build_log_simplifier.py b/development/build_log_simplifier/build_log_simplifier.py
index 34d6e9d..83f30adc 100755
--- a/development/build_log_simplifier/build_log_simplifier.py
+++ b/development/build_log_simplifier/build_log_simplifier.py
@@ -146,46 +146,6 @@
             prev_line_is_boring = False
     return result
 
-def remove_known_uninteresting_lines(lines):
-  skipLines = {
-      "A fine-grained performance profile is available: use the --scan option.",
-      "* Get more help at https://help.gradle.org",
-      "Use '--warning-mode all' to show the individual deprecation warnings.",
-      "See https://docs.gradle.org/6.5/userguide/command_line_interface.html#sec:command_line_warnings",
-
-      "Note: Some input files use or override a deprecated API.",
-      "Note: Recompile with -Xlint:deprecation for details.",
-      "Note: Some input files use unchecked or unsafe operations.",
-      "Note: Recompile with -Xlint:unchecked for details.",
-
-      "w: ATTENTION!",
-      "This build uses unsafe internal compiler arguments:",
-      "-XXLanguage:-NewInference",
-      "-XXLanguage:+InlineClasses",
-      "This mode is not recommended for production use,",
-      "as no stability/compatibility guarantees are given on",
-      "compiler or generated code. Use it at your own risk!"
-  }
-  skipPrefixes = [
-      "See the profiling report at:",
-
-      "Deprecated Gradle features were used in this build"
-  ]
-  result = []
-  for line in lines:
-      stripped = line.strip()
-      if stripped in skipLines:
-          continue
-      include = True
-      for prefix in skipPrefixes:
-          if stripped.startswith(prefix):
-              include = False
-              break
-      if include:
-          result.append(line)
-  return result
-
-
 # Returns the path of the config file holding exemptions for deterministic/consistent output.
 # These exemptions can be garbage collected via the `--gc` argument
 def get_deterministic_exemptions_path():
@@ -260,6 +220,23 @@
             prev_blank = False
     return result
 
+def extract_task_name(line):
+    prefix = "> Task "
+    if line.startswith(prefix):
+        return line[len(prefix):].strip()
+    return None
+
+def is_task_line(line):
+    return extract_task_name(line) is not None
+
+def extract_task_names(lines):
+    names = []
+    for line in lines:
+        name = extract_task_name(line)
+        if name is not None and name not in names:
+            names.append(name)
+    return names
+
 # If a task has no output (or only blank output), this function removes the task (and its output)
 # For example, turns this:
 #  > Task :a
@@ -277,8 +254,8 @@
     pending_task = None
     pending_blanks = []
     for line in lines:
-        is_task = line.startswith("> Task ") or line.startswith("> Configure project ")
-        if is_task:
+        is_section = is_task_line(line) or line.startswith("> Configure project ")
+        if is_section:
             pending_task = line
             pending_blanks = []
         elif line.strip() == "":
@@ -408,12 +385,12 @@
         if line == "":
             continue
         # save task name
-        is_task = False
-        if line.startswith("> Task :") or line.startswith("> Configure project "):
+        is_section = False
+        if is_task_line(line) or line.startswith("> Configure project "):
             # If a task creates output, we record its name
             line = "# " + line
             pending_task_line = line
-            is_task = True
+            is_section = True
         # determine where to put task name
         current_found_index = existing_matcher.index_first_matching_regex(line)
         if current_found_index is not None:
@@ -423,7 +400,7 @@
             pending_task_line = None
             continue
         # skip outputting task names for tasks that don't output anything
-        if is_task:
+        if is_section:
             continue
 
         # escape message
@@ -543,7 +520,6 @@
     if not validate:
         interesting_lines = select_failing_task_output(interesting_lines)
     interesting_lines = shorten_uninteresting_stack_frames(interesting_lines)
-    interesting_lines = remove_known_uninteresting_lines(interesting_lines)
     interesting_lines = remove_by_regexes(interesting_lines, exemption_regexes, validate)
     interesting_lines = collapse_tasks_having_no_output(interesting_lines)
     interesting_lines = collapse_consecutive_blank_lines(interesting_lines)
@@ -562,28 +538,29 @@
         if len(interesting_lines) != 0:
             print("")
             print("=" * 80)
-            print("build_log_simplifier.py: Error: Found " + str(len(interesting_lines)) + " lines of new warning output:")
+            print("build_log_simplifier.py: Error: Found " + str(len(interesting_lines)) + " new lines of warning output!")
             print("")
-            print("".join(interesting_lines))
-            print("=" * 80)
-            print("Error: build_log_simplifier.py found " + str(len(interesting_lines)) + " new lines of output")
+            print("The new output:")
+            print("  " + "  ".join(interesting_lines))
             print("")
-            print("  Log     : " + ",".join(log_paths))
-            print("  Baseline: " + get_deterministic_exemptions_path())
+            print("To reproduce this failure:")
+            print("  Try $ ./gradlew -Pandroidx.validateNoUnrecognizedMessages --rerun-tasks " + " ".join(extract_task_names(interesting_lines)))
+            print("")
+            print("Instructions:")
+            print("  Fix these messages if you can.")
+            print("  Otherwise, you may suppress them.")
+            print("  See also https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/development/build_log_simplifier/VALIDATION_FAILURE.md")
+            print("")
             new_exemptions_path = log_paths[0] + ".ignore"
             # filter out any inconsistently observed messages so we don't try to exempt them twice
             all_lines = remove_by_regexes(all_lines, flake_exemption_regexes, validate)
             # update deterministic exemptions file based on the result
             suggested = generate_suggested_exemptions(all_lines, deterministic_exemption_regexes, arguments.gc)
             writelines(new_exemptions_path, suggested)
-            print("")
-            print("Please fix or suppress these new messages in the tool that generates them.")
-            print("If you cannot, then you can exempt them by doing:")
-            print("")
-            print("  cp " + new_exemptions_path + " " + get_deterministic_exemptions_path())
-            print("")
-            print("For more information, see https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/development/build_log_simplifier/VALIDATION_FAILURE.md")
-            print("=" * 80)
+            print("Files:")
+            print("  Full Log                   : " + ",".join(log_paths))
+            print("  Baseline                   : " + get_deterministic_exemptions_path())
+            print("  Autogenerated new baseline : " + new_exemptions_path)
             exit(1)
     else:
         print("".join(interesting_lines))
diff --git a/development/build_log_simplifier/message-flakes.ignore b/development/build_log_simplifier/message-flakes.ignore
index 798e21b..eb09b8e 100644
--- a/development/build_log_simplifier/message-flakes.ignore
+++ b/development/build_log_simplifier/message-flakes.ignore
@@ -15,4 +15,4 @@
 Stream closed
 # > Task :compose:compiler:compiler-hosted:integration-tests:testDebugUnitTest
 # If a test fails, we don't want the build to fail, we want to pass the test output to the tests server and for the tests server to report the failure
-[0-9]+ tests .*failed.*
+[0-9]+ test.*failed.*
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index cf48ed2..95cf42b 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -42,6 +42,7 @@
 Use '\-\-warning\-mode all' to show the individual deprecation warnings\.
 See https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/command_line_interface\.html\#sec:command_line_warnings
 BUILD SUCCESSFUL in .*
+[0-9]+ actionable tasks: [0-9]+ up\-to\-date
 [0-9]+ actionable tasks: [0-9]+ executed
 [0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ from cache, [0-9]+ up\-to\-date
 [0-9]+ actionable task: [0-9]+ executed
@@ -399,6 +400,8 @@
 Html results of .* zipped into.*\.zip
 # > Task :annotation:annotation-experimental-lint:test
 WARNING: An illegal reflective access operation has occurred
+WARNING\: Illegal reflective access by org\.jetbrains\.kotlin\.com\.intellij\.util\.ReflectionUtil \(file\:\$CHECKOUT\/prebuilts\/androidx\/external\/org\/jetbrains\/kotlin\/kotlin\-compiler\-embeddable\/[0-9]+\.[0-9]+\.[0-9]+\/kotlin\-compiler\-embeddable\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
+WARNING\: Please consider reporting this to the maintainers of org\.jetbrains\.kotlin\.com\.intellij\.util\.ReflectionUtil
 WARNING: Illegal reflective access by com\.intellij\.util\.ReflectionUtil \(file:\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-compiler/[0-9]+\.[0-9]+\.[0-9]+/kotlin\-compiler\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
 WARNING: Illegal reflective access by androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion\$isFromCompiledClass\$[0-9]+ \(file:\$OUT_DIR/androidx/room/room\-compiler\-processing/build/libs/room\-compiler\-processing\-[0-9]+\.[0-9]+\.[0-9]+\-alpha[0-9]+\.jar\) to field com\.sun\.tools\.javac\.code\.Symbol\.owner
 WARNING: Illegal reflective access by com\.intellij\.util\.ReflectionUtil \(file:\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-compiler/[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+/kotlin\-compiler\-[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
@@ -407,13 +410,10 @@
 WARNING: Use \-\-illegal\-access=warn to enable warnings of further illegal reflective access operations
 WARNING: All illegal access operations will be denied in a future release
 # > Task :compose:compiler:compiler-hosted:integration-tests:testDebugUnitTest
-[0-9]+ tests completed, [0-9]+ failed, [0-9]+ skipped
 WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/[0-9]+\.[0-9]+\-alpha\-[0-9]+/shadowapi\-[0-9]+\.[0-9]+\-alpha\-[0-9]+\.jar\) to field java\.lang\.reflect\.Field\.modifiers
 WARNING: Please consider reporting this to the maintainers of org\.robolectric\.util\.ReflectionHelpers
 # > Task :compose:compiler:compiler-hosted:integration-tests:test
 wrote dependency log to \$DIST_DIR/affected_module_detector_log\.txt
-Deprecated Gradle features were used in this build\, making it incompatible with Gradle [0-9]+\.[0-9]+\.
-Use \'\-\-warning\-mode all\' to show the individual deprecation warnings\.
 # > Task :enterprise-feedback-testing:compileDebugUnitTestJavaWithJavac
 Note: \$SUPPORT/enterprise/feedback/testing/src/test/java/androidx/enterprise/feedback/FakeKeyedAppStatesReporterTest\.java uses or overrides a deprecated API\.
 # > Task :biometric:biometric:compileDebugUnitTestJavaWithJavac
@@ -445,9 +445,6 @@
 WARNING: Please consider reporting this to the maintainers of androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion\$isFromCompiledClass\$[0-9]+
 # > Task :wear:wear-watchface-complications-rendering:compileDebugUnitTestJavaWithJavac
 Note\: \$SUPPORT\/wear\/wear\-watchface\-complications\-rendering\/src\/test\/java\/androidx\/wear\/watchface\/complications\/rendering\/RoundedDrawableTest\.java uses or overrides a deprecated API\.
-# > Task :wear:wear-watchface:testDebugUnitTest
-System\.logW\: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable\: Explicit termination method \'dispose\' not called
-System\.logW\: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable\: Explicit termination method \'release\' not called
 # > Task :benchmark:benchmark-perfetto:mergeDebugAndroidTestJavaResource
 More than one file was found with OS independent path '.*'\. This version of the Android Gradle Plugin chooses the file from the app or dynamic\-feature module, but this can cause unexpected behavior or errors at runtime\. Future versions of the Android Gradle Plugin will throw an error in this case\.
 # > Task :docs-runner:dokkaJavaTipOfTreeDocs
@@ -466,7 +463,6 @@
 # > Task :annotation:annotation-experimental-lint:compileKotlin
 w\: ATTENTION\!
 This build uses unsafe internal compiler arguments\:
-\-XXLanguage\:\-NewInference
 This mode is not recommended for production use\,
 as no stability\/compatibility guarantees are given on
 compiler or generated code\. Use it at your own risk\!
@@ -609,7 +605,6 @@
 w: \$SUPPORT/navigation/navigation\-dynamic\-features\-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment\.kt: \([0-9]+, [0-9]+\): 'startIntentSenderForResult\(IntentSender!, Int, Intent\?, Int, Int, Int, Bundle\?\): Unit' is deprecated\. Deprecated in Java
 w: \$SUPPORT/navigation/navigation\-dynamic\-features\-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment\.kt: \([0-9]+, [0-9]+\): 'onActivityResult\(Int, Int, Intent\?\): Unit' is deprecated\. Deprecated in Java
 # > Task :inspection:inspection-gradle-plugin:test
-[0-9]+ test completed, [0-9]+ failed
 There were failing tests\. See the report at: .*.html
 # > Task :compose:ui:ui:processDebugUnitTestManifest
 \$OUT_DIR/androidx/compose/ui/ui/build/intermediates/tmp/manifest/test/debug/manifestMerger[0-9]+\.xml Warning:
@@ -643,14 +638,30 @@
 Stripped invalid locals information from [0-9]+ method\.
 Stripped invalid locals information from [0-9]+ methods\.
 Methods with invalid locals information:
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitVerticalTouchSlopOrCancellation\-s[0-9]+qLkbw\(androidx\.compose\.ui\.input\.pointer\.HandlePointerInputScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancel\(androidx\.compose\.ui\.input\.pointer\.HandlePointerInputScope, kotlin\.coroutines\.Continuation\)
+java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitVerticalTouchSlopOrCancellation\-qFc19kk\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
+java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancel\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlin\.coroutines\.Continuation\)
 Type information in locals\-table is inconsistent\. Cannot constrain type: BOTTOM \(empty\) for value: v[0-9]+ by constraint INT\.
 Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
 Type information in locals\-table is inconsistent\. Cannot constrain type: BOTTOM \(empty\) for value: v[0-9]+ by constraint FLOAT\.
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitHorizontalTouchSlopOrCancellation\-s[0-9]+qLkbw\(androidx\.compose\.ui\.input\.pointer\.HandlePointerInputScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
+java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitHorizontalTouchSlopOrCancellation\-qFc19kk\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
 java\.lang\.Object androidx\.compose\.foundation\.gestures\.MultitouchGestureDetectorKt\$detectMultitouchGestures\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
 Attempt to define local of type int as it\$iv:java\.lang\.Object
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancellation\(androidx\.compose\.ui\.input\.pointer\.HandlePointerInputScope, kotlin\.coroutines\.Continuation\)
+Type information in locals\-table is inconsistent\. Cannot constrain type: INT for value: v380\(index\$iv\$iv\) by constraint FLOAT\.
+java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancellation\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlin\.coroutines\.Continuation\)
 # > Task :hilt:integration-tests:hilt-testapp-viewmodel:kaptDebugKotlin
-warning: The following options were not recognized by any processor: '\[dagger\.fastInit, dagger\.hilt\.android\.internal\.disableAndroidSuperclassValidation, kapt\.kotlin\.generated\]'
\ No newline at end of file
+warning: The following options were not recognized by any processor: '\[dagger\.fastInit, dagger\.hilt\.android\.internal\.disableAndroidSuperclassValidation, kapt\.kotlin\.generated\]'
+# > Task :preference:preference:compileDebugAndroidTestKotlin
+w\: \$SUPPORT\/preference\/preference\/src\/androidTest\/java\/androidx\/preference\/tests\/PreferenceDialogFragmentCompatTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTargetFragment\(Fragment\?\, Int\)\: Unit\' is deprecated\. Deprecated in Java
+# > Task :viewpager2:viewpager2:compileDebugAndroidTestKotlin
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/HostFragmentBackStackTest\.kt\: \([0-9]+\, [0-9]+\)\: \'enableDebugLogging\(Boolean\)\: Unit\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'getter for systemWindowInsets\: Insets\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'getter for stableInsets\: Insets\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setSystemWindowInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setSystemGestureInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setStableInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTappableElementInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setMandatorySystemGestureInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeSystemWindowInsets\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: Unnecessary non\-null assertion \(\!\!\) on a non\-null receiver of type WindowInsetsCompat
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeStableInsets\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeDisplayCutout\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
\ No newline at end of file
diff --git a/development/build_log_simplifier/test.py b/development/build_log_simplifier/test.py
index 8a3b4a4..c8ff201 100755
--- a/development/build_log_simplifier/test.py
+++ b/development/build_log_simplifier/test.py
@@ -16,6 +16,7 @@
 
 from build_log_simplifier import collapse_consecutive_blank_lines
 from build_log_simplifier import collapse_tasks_having_no_output
+from build_log_simplifier import extract_task_names
 from build_log_simplifier import remove_unmatched_exemptions
 from build_log_simplifier import suggest_missing_exemptions
 from build_log_simplifier import normalize_paths
@@ -70,6 +71,23 @@
     assert(matcher.index_first_matching_regex("single") == 2)
     assert(matcher.index_first_matching_regex("absent") is None)
 
+def test_detect_task_names():
+    print("test_detect_task_names")
+    lines = [
+        "> Task :one\n",
+        "some output\n",
+        "> Task :two\n",
+        "more output\n"
+    ]
+    task_names = [":one", ":two"]
+    detected_names = extract_task_names(lines)
+    if detected_names != task_names:
+        fail("extract_task_names returned incorrect response\n" +
+            "Input   : " + str(lines) + "\n" +
+            "Output  : " + str(detected_names) + "\n" +
+            "Expected: " + str(task_names)
+        )
+
 def test_remove_unmatched_exemptions():
     print("test_remove_unmatched_exemptions")
     lines = [
@@ -261,6 +279,7 @@
 def main():
     test_collapse_consecutive_blank_lines()
     test_collapse_tasks_having_no_output()
+    test_detect_task_names()
     test_suggest_missing_exemptions()
     test_normalize_paths()
     test_regexes_matcher_get_matching_regexes()
diff --git a/development/simplify-build-failure/simplify-build-failure.sh b/development/simplify-build-failure/simplify-build-failure.sh
index ddd75ab..734a109 100755
--- a/development/simplify-build-failure/simplify-build-failure.sh
+++ b/development/simplify-build-failure/simplify-build-failure.sh
@@ -363,6 +363,9 @@
   else
     failed
   fi
+  echo Copying minimal set of files into $fewestFilesOutputPath
+  rm -rf "$fewestFilesOutputPath"
+  cp -rT "$filtererStep1Output" "$fewestFilesOutputPath"
 fi
 
 if [ "$subfilePath" == "" ]; then
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 5a145f5..3a97ddb 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -4,8 +4,8 @@
 }
 
 dependencies {
-    docs("androidx.activity:activity:1.2.0-beta01")
-    docs("androidx.activity:activity-ktx:1.2.0-beta01")
+    docs("androidx.activity:activity:1.2.0-beta02")
+    docs("androidx.activity:activity-ktx:1.2.0-beta02")
     docs("androidx.ads:ads-identifier:1.0.0-alpha04")
     docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
     docs("androidx.annotation:annotation:1.2.0-alpha01")
@@ -19,8 +19,9 @@
     docs("androidx.autofill:autofill:1.1.0-rc01")
     docs("androidx.benchmark:benchmark-common:1.1.0-alpha01")
     docs("androidx.benchmark:benchmark-junit4:1.1.0-alpha01")
-    docs("androidx.biometric:biometric:1.1.0-rc01")
-    docs("androidx.browser:browser:1.3.0-rc01")
+    docs("androidx.biometric:biometric:1.2.0-alpha01")
+    docs("androidx.biometric:biometric-ktx:1.2.0-alpha01")
+    docs("androidx.browser:browser:1.3.0")
     docs("androidx.camera:camera-camera2:1.0.0-beta12")
     docs("androidx.camera:camera-core:1.0.0-beta12")
     docs("androidx.camera:camera-extensions:1.0.0-alpha19")
@@ -30,41 +31,44 @@
     docs("androidx.cardview:cardview:1.0.0")
     docs("androidx.collection:collection:1.1.0")
     docs("androidx.collection:collection-ktx:1.1.0")
-    docs("androidx.compose.animation:animation:1.0.0-alpha07")
-    docs("androidx.compose.animation:animation-core:1.0.0-alpha07")
-    samples("androidx.compose.animation:animation-samples:1.0.0-alpha07")
-    samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha07")
-    docs("androidx.compose.foundation:foundation:1.0.0-alpha07")
-    docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha07")
-    samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha07")
-    samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha07")
-    docs("androidx.compose.material:material:1.0.0-alpha07")
-    docs("androidx.compose.material:material-icons-core:1.0.0-alpha07")
-    samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha07")
-    docs("androidx.compose.material:material-icons-extended:1.0.0-alpha07")
-    samples("androidx.compose.material:material-samples:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-geometry:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-graphics:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-text:1.0.0-alpha07")
+    docs("androidx.compose.animation:animation:1.0.0-alpha08")
+    docs("androidx.compose.animation:animation-core:1.0.0-alpha08")
+    samples("androidx.compose.animation:animation-samples:1.0.0-alpha08")
+    samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha08")
+    docs("androidx.compose.foundation:foundation:1.0.0-alpha08")
+    docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha08")
+    samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha08")
+    samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha08")
+    docs("androidx.compose.material:material:1.0.0-alpha08")
+    docs("androidx.compose.material:material-icons-core:1.0.0-alpha08")
+    samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha08")
+    docs("androidx.compose.material:material-icons-extended:1.0.0-alpha08")
+    samples("androidx.compose.material:material-samples:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-rxjava3:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-geometry:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-graphics:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-test:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-test-junit4:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-text:1.0.0-alpha08")
     docs("androidx.compose.ui:ui-text-android:1.0.0-alpha06")
-    samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-unit:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-util:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-samples:1.0.0-alpha07")
+    samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-unit:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-util:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-samples:1.0.0-alpha08")
     docs("androidx.concurrent:concurrent-futures:1.1.0")
     docs("androidx.concurrent:concurrent-futures-ktx:1.1.0")
     docs("androidx.contentpager:contentpager:1.0.0")
@@ -76,10 +80,10 @@
     docs("androidx.core:core-ktx:1.5.0-alpha05")
     docs("androidx.cursoradapter:cursoradapter:1.0.0")
     docs("androidx.customview:customview:1.1.0")
-    docs("androidx.datastore:datastore:1.0.0-alpha04")
-    docs("androidx.datastore:datastore-core:1.0.0-alpha04")
-    docs("androidx.datastore:datastore-preferences:1.0.0-alpha04")
-    docs("androidx.datastore:datastore-preferences-core:1.0.0-alpha04")
+    docs("androidx.datastore:datastore:1.0.0-alpha05")
+    docs("androidx.datastore:datastore-core:1.0.0-alpha05")
+    docs("androidx.datastore:datastore-preferences:1.0.0-alpha05")
+    docs("androidx.datastore:datastore-preferences-core:1.0.0-alpha05")
     docs("androidx.documentfile:documentfile:1.0.0")
     docs("androidx.drawerlayout:drawerlayout:1.1.1")
     docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
@@ -87,9 +91,9 @@
     docs("androidx.emoji:emoji:1.2.0-alpha01")
     docs("androidx.emoji:emoji-appcompat:1.2.0-alpha01")
     docs("androidx.emoji:emoji-bundled:1.2.0-alpha01")
-    docs("androidx.enterprise:enterprise-feedback:1.1.0-beta01")
-    docs("androidx.enterprise:enterprise-feedback-testing:1.1.0-beta01")
-    docs("androidx.exifinterface:exifinterface:1.3.1")
+    docs("androidx.enterprise:enterprise-feedback:1.1.0-rc01")
+    docs("androidx.enterprise:enterprise-feedback-testing:1.1.0-rc01")
+    docs("androidx.exifinterface:exifinterface:1.3.2")
     docs("androidx.fragment:fragment:1.3.0-beta01")
     docs("androidx.fragment:fragment-ktx:1.3.0-beta01")
     docs("androidx.fragment:fragment-testing:1.3.0-beta01")
@@ -99,10 +103,10 @@
     docs("androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02")
     docs("androidx.hilt:hilt-work:1.0.0-alpha02")
     docs("androidx.interpolator:interpolator:1.0.0")
-    docs("androidx.leanback:leanback:1.1.0-alpha05")
-    docs("androidx.leanback:leanback-paging:1.1.0-alpha05")
-    docs("androidx.leanback:leanback-preference:1.1.0-alpha05")
-    docs("androidx.leanback:leanback-tab:1.1.0-alpha05")
+    docs("androidx.leanback:leanback:1.1.0-beta01")
+    docs("androidx.leanback:leanback-paging:1.1.0-alpha06")
+    docs("androidx.leanback:leanback-preference:1.1.0-beta01")
+    docs("androidx.leanback:leanback-tab:1.1.0-beta01")
     docs("androidx.lifecycle:lifecycle-common:2.3.0-beta01")
     docs("androidx.lifecycle:lifecycle-common-java8:2.3.0-beta01")
     docs("androidx.lifecycle:lifecycle-extensions:2.2.0")
@@ -121,36 +125,36 @@
     docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
     docs("androidx.loader:loader:1.1.0")
     docs("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01")
-    docs("androidx.media2:media2-common:1.1.0-rc01")
-    docs("androidx.media2:media2-player:1.1.0-rc01")
-    docs("androidx.media2:media2-session:1.1.0-rc01")
-    docs("androidx.media2:media2-widget:1.1.0-rc01")
-    docs("androidx.media:media:1.2.0")
+    docs("androidx.media2:media2-common:1.1.0")
+    docs("androidx.media2:media2-player:1.1.0")
+    docs("androidx.media2:media2-session:1.1.0")
+    docs("androidx.media2:media2-widget:1.1.0")
+    docs("androidx.media:media:1.2.1")
     docs("androidx.mediarouter:mediarouter:1.2.0")
-    docs("androidx.navigation:navigation-common:2.3.1")
-    docs("androidx.navigation:navigation-common-ktx:2.3.1")
-    docs("androidx.navigation:navigation-compose:1.0.0-alpha02")
+    docs("androidx.navigation:navigation-common:2.3.2")
+    docs("androidx.navigation:navigation-common-ktx:2.3.2")
+    docs("androidx.navigation:navigation-compose:1.0.0-alpha03")
     samples("androidx.navigation:navigation-compose-samples:1.0.0-alpha01")
-    docs("androidx.navigation:navigation-dynamic-features-fragment:2.3.1")
-    docs("androidx.navigation:navigation-dynamic-features-runtime:2.3.1")
-    docs("androidx.navigation:navigation-fragment:2.3.1")
-    docs("androidx.navigation:navigation-fragment-ktx:2.3.1")
-    docs("androidx.navigation:navigation-runtime:2.3.1")
-    docs("androidx.navigation:navigation-runtime-ktx:2.3.1")
-    docs("androidx.navigation:navigation-testing:2.3.1")
-    docs("androidx.navigation:navigation-ui:2.3.1")
-    docs("androidx.navigation:navigation-ui-ktx:2.3.1")
-    docs("androidx.paging:paging-common:3.0.0-alpha09")
-    docs("androidx.paging:paging-common-ktx:3.0.0-alpha09")
-    docs("androidx.paging:paging-compose:1.0.0-alpha02")
+    docs("androidx.navigation:navigation-dynamic-features-fragment:2.3.2")
+    docs("androidx.navigation:navigation-dynamic-features-runtime:2.3.2")
+    docs("androidx.navigation:navigation-fragment:2.3.2")
+    docs("androidx.navigation:navigation-fragment-ktx:2.3.2")
+    docs("androidx.navigation:navigation-runtime:2.3.2")
+    docs("androidx.navigation:navigation-runtime-ktx:2.3.2")
+    docs("androidx.navigation:navigation-testing:2.3.2")
+    docs("androidx.navigation:navigation-ui:2.3.2")
+    docs("androidx.navigation:navigation-ui-ktx:2.3.2")
+    docs("androidx.paging:paging-common:3.0.0-alpha10")
+    docs("androidx.paging:paging-common-ktx:3.0.0-alpha10")
+    docs("androidx.paging:paging-compose:1.0.0-alpha03")
     samples("androidx.paging:paging-compose-samples:3.0.0-alpha08")
-    docs("androidx.paging:paging-guava:3.0.0-alpha09")
-    docs("androidx.paging:paging-runtime:3.0.0-alpha09")
-    docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha09")
-    docs("androidx.paging:paging-rxjava2:3.0.0-alpha09")
-    docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha09")
-    docs("androidx.paging:paging-rxjava3:3.0.0-alpha09")
-    samples("androidx.paging:paging-samples:3.0.0-alpha09")
+    docs("androidx.paging:paging-guava:3.0.0-alpha10")
+    docs("androidx.paging:paging-runtime:3.0.0-alpha10")
+    docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha10")
+    docs("androidx.paging:paging-rxjava2:3.0.0-alpha10")
+    docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha10")
+    docs("androidx.paging:paging-rxjava3:3.0.0-alpha10")
+    samples("androidx.paging:paging-samples:3.0.0-alpha10")
     docs("androidx.palette:palette:1.0.0")
     docs("androidx.palette:palette-ktx:1.0.0")
     docs("androidx.percentlayout:percentlayout:1.0.1")
@@ -158,7 +162,7 @@
     docs("androidx.preference:preference-ktx:1.1.1")
     docs("androidx.print:print:1.1.0-beta01")
     docs("androidx.recommendation:recommendation:1.0.0")
-    docs("androidx.recyclerview:recyclerview:1.2.0-alpha06")
+    docs("androidx.recyclerview:recyclerview:1.2.0-beta01")
     docs("androidx.recyclerview:recyclerview-selection:2.0.0-alpha01")
     docs("androidx.remotecallback:remotecallback:1.0.0-alpha02")
     docs("androidx.room:room-common:2.3.0-alpha03")
@@ -170,10 +174,10 @@
     docs("androidx.room:room-testing:2.3.0-alpha03")
     docs("androidx.savedstate:savedstate:1.1.0-beta01")
     docs("androidx.savedstate:savedstate-ktx:1.1.0-beta01")
-    docs("androidx.security:security-crypto:1.1.0-alpha02")
+    docs("androidx.security:security-crypto:1.1.0-alpha03")
     docs("androidx.security:security-crypto-ktx:1.1.0-alpha02")
     docs("androidx.security:security-identity-credential:1.0.0-alpha01")
-    docs("androidx.sharetarget:sharetarget:1.1.0-beta01")
+    docs("androidx.sharetarget:sharetarget:1.1.0-rc01")
     docs("androidx.slice:slice-builders:1.1.0-alpha01")
     docs("androidx.slice:slice-builders-ktx:1.0.0-alpha07")
     docs("androidx.slice:slice-core:1.1.0-alpha01")
@@ -188,7 +192,8 @@
     docs("androidx.textclassifier:textclassifier:1.0.0-alpha03")
     docs("androidx.tracing:tracing:1.0.0")
     docs("androidx.tracing:tracing-ktx:1.0.0")
-    docs("androidx.transition:transition:1.4.0-beta01")
+    docs("androidx.transition:transition:1.4.0-rc01")
+    docs("androidx.transition:transition-ktx:1.4.0-rc01")
     docs("androidx.tvprovider:tvprovider:1.1.0-alpha01")
     docs("androidx.vectordrawable:vectordrawable:1.2.0-alpha02")
     docs("androidx.vectordrawable:vectordrawable-animated:1.2.0-alpha01")
@@ -196,27 +201,27 @@
     docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
     docs("androidx.viewpager2:viewpager2:1.1.0-alpha01")
     docs("androidx.viewpager:viewpager:1.0.0")
-    docs("androidx.wear:wear:1.2.0-alpha02")
+    docs("androidx.wear:wear:1.2.0-alpha03")
     stubs(fileTree(dir: '../wear/wear_stubs/', include: ['com.google.android.wearable-stubs.jar']))
-    docs("androidx.wear:wear-complications-data:1.0.0-alpha02")
-    docs("androidx.wear:wear-complications-provider:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-client:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-data:1.0.0-alpha02")
+    docs("androidx.wear:wear-complications-data:1.0.0-alpha03")
+    docs("androidx.wear:wear-complications-provider:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface-client:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface-data:1.0.0-alpha03")
     samples("androidx.wear:wear-watchface-samples:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-style:1.0.0-alpha02")
-    docs("androidx.wear:wear-input:1.0.0-rc01")
-    docs("androidx.wear:wear-input-testing:1.0.0-rc01")
-    docs("androidx.webkit:webkit:1.4.0-rc01")
+    docs("androidx.wear:wear-watchface-style:1.0.0-alpha03")
+    docs("androidx.wear:wear-input:1.0.0")
+    docs("androidx.wear:wear-input-testing:1.0.0")
+    docs("androidx.webkit:webkit:1.4.0-rc02")
     docs("androidx.window:window:1.0.0-alpha01")
     stubs(fileTree(dir: '../window/stubs/', include: ['window-sidecar-release-0.1.0-alpha01.aar']))
     stubs(project(":window:window-extensions"))
-    docs("androidx.work:work-gcm:2.5.0-beta01")
-    docs("androidx.work:work-multiprocess:2.5.0-beta01")
-    docs("androidx.work:work-runtime:2.5.0-beta01")
-    docs("androidx.work:work-runtime-ktx:2.5.0-beta01")
-    docs("androidx.work:work-rxjava2:2.5.0-beta01")
-    docs("androidx.work:work-rxjava3:2.5.0-beta01")
-    docs("androidx.work:work-testing:2.5.0-beta01")
+    docs("androidx.work:work-gcm:2.5.0-beta02")
+    docs("androidx.work:work-multiprocess:2.5.0-beta02")
+    docs("androidx.work:work-runtime:2.5.0-beta02")
+    docs("androidx.work:work-runtime-ktx:2.5.0-beta02")
+    docs("androidx.work:work-rxjava2:2.5.0-beta02")
+    docs("androidx.work:work-rxjava3:2.5.0-beta02")
+    docs("androidx.work:work-testing:2.5.0-beta02")
 }
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 440faef..e2d581f 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -191,6 +191,7 @@
     docs(project(":room:room-testing"))
     docs(project(":savedstate:savedstate"))
     docs(project(":savedstate:savedstate-ktx"))
+    docs(project(":security:security-app-authenticator"))
     docs(project(":security:security-biometric"))
     docs(project(":security:security-crypto"))
     docs(project(":security:security-crypto-ktx"))
diff --git a/docs/api_guidelines.md b/docs/api_guidelines.md
new file mode 100644
index 0000000..3781200
--- /dev/null
+++ b/docs/api_guidelines.md
@@ -0,0 +1,1522 @@
+# Library API guidelines
+
+[TOC]
+
+## Introduction {#introduction}
+
+s.android.com/api-guidelines,
+which covers standard and practices for designing platform APIs.
+
+All platform API design guidelines also apply to Jetpack libraries, with any
+additional guidelines or exceptions noted in this document. Jetpack libraries
+also follow
+[explicit API mode](https://kotlinlang.org/docs/reference/whatsnew14.html#explicit-api-mode-for-library-authors)
+for Kotlin libraries.
+
+## Modules {#module}
+
+### Packaging and naming {#module-naming}
+
+Java packages within Jetpack follow the format `androidx.<feature-name>`. All
+classes within a feature's artifact must reside within this package, and may
+further subdivide into `androidx.<feature-name>.<layer>` using standard Android
+layers (app, widget, etc.) or layers specific to the feature.
+
+Maven artifacts use the groupId format `androidx.<feature-name>` and artifactId
+format `<feature-name>` to match the Java package.
+
+Sub-features that can be separated into their own artifact should use the
+following formats:
+
+Java package: `androidx.<feature-name>.<sub-feature>.<layer>`
+
+Maven groupId: `androidx.<feature-name>`
+
+Maven artifactId: `<feature-name>-<sub-feature>`
+
+#### Common sub-feature names {#module-naming-subfeature}
+
+*   `-testing` for an artifact intended to be used while testing usages of your
+    library, e.g. `androidx.room:room-testing`
+*   `-core` for a low-level artifact that *may* contain public APIs but is
+    primarily intended for use by other libraries in the group
+*   `-ktx` for an Kotlin artifact that exposes idiomatic Kotlin APIs as an
+    extension to a Java-only library
+*   `-java8` for a Java 8 artifact that exposes idiomatic Java 8 APIs as an
+    extension to a Java 7 library
+*   `-<third-party>` for an artifact that integrates an optional third-party API
+    surface, e.g. `-proto` or `-rxjava2`. Note that a major version is included
+    in the sub-feature name for third-party API surfaces where the major version
+    indicates binary compatibility (only needed for post-1.x).
+
+Artifacts **should not** use `-impl` or `-base` to indicate that a library is an
+implementation detail shared within the group. Instead, use `-core`.
+
+#### Splitting existing modules
+
+Existing modules _should not_ be split into smaller modules; doing so creates
+the potential for class duplication issues when a developer depends on a new
+sub-module alongside the older top-level module. Consider the following
+scenario:
+
+*   `androidx.library:1.0.0`
+    *   contains classes `androidx.library.A` and `androidx.library.util.B`
+
+This module is split, moving `androidx.library.util.B` to a new module:
+
+*   `androidx.library:1.1.0`
+    *   contains class `androidx.library.A`
+    *   depends on `androidx.library.util:1.0.0`
+*   `androidx.library.util:1.0.0`
+    *   depends on `androidx.library.util.B`
+
+A developer writes an app that depends directly on `androidx.library.util:1.0.0`
+and transitively pulls in `androidx.library:1.0.0`. Their app will no longer
+compile due to class duplication of `androidx.library.util.B`.
+
+While it is possible for the developer to fix this by manually specifying a
+dependency on `androidx.library:1.1.0`, there is no easy way for the developer
+to discover this solution from the class duplication error raised at compile
+time.
+
+#### Same-version (atomic) groups
+
+Library groups are encouraged to opt-in to a same-version policy whereby all
+libraries in the group use the same version and express exact-match dependencies
+on libraries within the group. Such groups must increment the version of every
+library at the same time and release all libraries at the same time.
+
+Atomic groups are specified in
+[`LibraryGroups.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt):
+
+```kotlin
+// Non-atomic library group
+val APPCOMPAT = LibraryGroup("androidx.appcompat", null)
+// Atomic library group
+val APPSEARCH = LibraryGroup("androidx.appsearch", LibraryVersions.APPSEARCH)
+```
+
+Libraries within an atomic group should not specify a version in their
+`build.gradle`:
+
+```groovy
+androidx {
+    name = 'AppSearch'
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.APPSEARCH
+    inceptionYear = '2019'
+    description = 'Provides local and centralized app indexing'
+}
+```
+
+There is one exception to this policy. Newly-added libraries within an atomic
+group may stay within the `1.0.0-alphaXX` before conforming to the same-version
+policy. When the library would like to move to `beta`, it must match the version
+used by the atomic group (which must be `beta` at the time).
+
+The benefits of using an atomic group are:
+
+-   Easier for developers to understand dependency versioning
+-   `@RestrictTo(LIBRARY_GROUP)` APIs are treated as private APIs and not
+    tracked for binary compatibility
+-   `@RequiresOptIn` APIs defined within the group may be used without any
+    restrictions between libraries in the group
+
+Potential drawbacks include:
+
+-   All libraries within the group must be versioned identically at head
+-   All libraries within the group must release at the same time
+
+
+### Choosing a `minSdkVersion` {#module-minsdkversion}
+
+The recommended minimum SDK version for new Jetpack libraries is currently
+**17** (Android 4.2, Jelly Bean). This SDK was chosen to represent 99% of active
+devices based on Play Store check-ins (see Android Studio
+[distribution metadata](https://dl.google.com/android/studio/metadata/distributions.json)
+for current statistics). This maximizes potential users for external developers
+while minimizing the amount of overhead necessary to support legacy versions.
+
+However, if no explicit minimum SDK version is specified for a library, the
+default is 14.
+
+Note that a library **must not** depend on another library with a higher
+`minSdkVersion` that its own, so it may be necessary for a new library to match
+its dependent libraries' `minSdkVersion`.
+
+Individual modules may choose a higher minimum SDK version for business or
+technical reasons. This is common for device-specific modules such as Auto or
+Wear.
+
+Individual classes or methods may be annotated with the
+[@RequiresApi](https://developer.android.com/reference/android/annotation/RequiresApi.html)
+annotation to indicate divergence from the overall module's minimum SDK version.
+Note that this pattern is _not recommended_ because it leads to confusion for
+external developers and should be considered a last-resort when backporting
+behavior is not feasible.
+
+## Platform compatibility API patterns {#platform-compatibility-apis}
+
+### Static shims (ex. [ViewCompat](https://developer.android.com/reference/android/support/v4/view/ViewCompat.html)) {#static-shim}
+
+When to use?
+
+*   Platform class exists at module's `minSdkVersion`
+*   Compatibility implementation does not need to store additional metadata
+
+Implementation requirements
+
+*   Class name **must** be `<PlatformClass>Compat`
+*   Package name **must** be `androidx.<feature>.<platform.package>`
+*   Superclass **must** be `Object`
+*   Class **must** be non-instantiable, i.e. constructor is private no-op
+*   Static fields and static methods **must** match match signatures with
+    `PlatformClass`
+    *   Static fields that can be inlined, ex. integer constants, **must not**
+        be shimmed
+*   Public method names **must** match platform method names
+*   Public methods **must** be static and take `PlatformClass` as first
+    parameter
+*   Implementation _may_ delegate to `PlatformClass` methods when available
+
+#### Sample {#static-shim-sample}
+
+The following sample provides static helper methods for the platform class
+`android.os.Process`.
+
+```java
+/**
+ * Helper for accessing features in {@link Process}.
+ */
+public final class ProcessCompat {
+    private ProcessCompat() {
+        // This class is non-instantiable.
+    }
+
+    /**
+     * [Docs should match platform docs.]
+     *
+     * Compatibility behavior:
+     * <ul>
+     * <li>SDK 24 and above, this method matches platform behavior.
+     * <li>SDK 16 through 23, this method is a best-effort to match platform behavior, but may
+     * default to returning {@code true} if an accurate result is not available.
+     * <li>SDK 15 and below, this method always returns {@code true} as application UIDs and
+     * isolated processes did not exist yet.
+     * </ul>
+     *
+     * @param [match platform docs]
+     * @return [match platform docs], or a value based on platform-specific fallback behavior
+     */
+    public static boolean isApplicationUid(int uid) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return Api24Impl.isApplicationUid(uid);
+        } else if (Build.VERSION.SDK_INT >= 17) {
+            return Api17Impl.isApplicationUid(uid);
+        } else if (Build.VERSION.SDK_INT == 16) {
+            return Api16Impl.isApplicationUid(uid);
+        } else {
+            return true;
+        }
+    }
+
+    @RequiresApi(24)
+    static class Api24Impl {
+        static boolean isApplicationUid(int uid) {
+            // In N, the method was made public on android.os.Process.
+            return Process.isApplicationUid(uid);
+        }
+    }
+
+    @RequiresApi(17)
+    static class Api17Impl {
+        private static Method sMethod_isAppMethod;
+        private static boolean sResolved;
+
+        static boolean isApplicationUid(int uid) {
+            // In JELLY_BEAN_MR2, the equivalent isApp(int) hidden method moved to public class
+            // android.os.UserHandle.
+            try {
+                if (!sResolved) {
+                    sResolved = true;
+                    sMethod_isAppMethod = UserHandle.class.getDeclaredMethod("isApp",int.class);
+                }
+                if (sMethod_isAppMethod != null) {
+                    return (Boolean) sMethod_isAppMethod.invoke(null, uid);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            return true;
+        }
+    }
+
+    ...
+}
+```
+
+### Wrapper (ex. [AccessibilityNodeInfoCompat](https://developer.android.com/reference/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.html)) {#wrapper}
+
+When to use?
+
+*   Platform class may not exist at module's `minSdkVersion`
+*   Compatibility implementation may need to store additional metadata
+*   Needs to integrate with platform APIs as return value or method argument
+*   **Note:** Should be avoided when possible, as using wrapper classes makes it
+    very difficult to deprecate classes and migrate source code when the
+    `minSdkVersion` is raised
+
+#### Sample {#wrapper-sample}
+
+The following sample wraps a hypothetical platform class `ModemInfo` that was
+added to the platform SDK in API level 23:
+
+```java
+public final class ModemInfoCompat {
+  // Only guaranteed to be non-null on SDK_INT >= 23. Note that referencing the
+  // class itself directly is fine -- only references to class members need to
+  // be pushed into static inner classes.
+  private final ModemInfo wrappedObj;
+
+  /**
+   * [Copy platform docs for matching constructor.]
+   */
+  public ModemInfoCompat() {
+    if (SDK_INT >= 23) {
+      wrappedObj = Api23Impl.create();
+    } else {
+      wrappedObj = null;
+    }
+    ...
+  }
+
+  @RequiresApi(23)
+  private ModemInfoCompat(@NonNull ModemInfo obj) {
+    mWrapped = obj;
+  }
+
+  /**
+   * Provides a backward-compatible wrapper for {@link ModemInfo}.
+   * <p>
+   * This method is not supported on devices running SDK < 23 since the platform
+   * class will not be available.
+   *
+   * @param info platform class to wrap
+   * @return wrapped class, or {@code null} if parameter is {@code null}
+   */
+  @RequiresApi(23)
+  @NonNull
+  public static ModemInfoCompat toModemInfoCompat(@NonNull ModemInfo info) {
+    return new ModemInfoCompat(obj);
+  }
+
+  /**
+   * Provides the {@link ModemInfo} represented by this object.
+   * <p>
+   * This method is not supported on devices running SDK < 23 since the platform
+   * class will not be available.
+   *
+   * @return platform class object
+   * @see ModemInfoCompat#toModemInfoCompat(ModemInfo)
+   */
+  @RequiresApi(23)
+  @NonNull
+  public ModemInfo toModemInfo() {
+    return mWrapped;
+  }
+
+  /**
+   * [Docs should match platform docs.]
+   *
+   * Compatibility behavior:
+   * <ul>
+   * <li>API level 23 and above, this method matches platform behavior.
+   * <li>API level 18 through 22, this method ...
+   * <li>API level 17 and earlier, this method always returns false.
+   * </ul>
+   *
+   * @return [match platform docs], or platform-specific fallback behavior
+   */
+  public boolean isLteSupported() {
+    if (SDK_INT >= 23) {
+      return Api23Impl.isLteSupported(mWrapped);
+    } else if (SDK_INT >= 18) {
+      // Smart fallback behavior based on earlier APIs.
+      ...
+    }
+    // Default behavior.
+    return false;
+  }
+
+  // All references to class members -- including the constructor -- must be
+  // made on an inner class to avoid soft-verification errors that slow class
+  // loading and prevent optimization.
+  @RequiresApi(23)
+  private static class Api23Impl {
+    @NonNull
+    static ModemInfo create() {
+      return new ModemInfo();
+    }
+
+    static boolean isLteSupported(PlatformClass obj) {
+      return obj.isLteSupported();
+    }
+  }
+}
+```
+
+Note that libraries written in Java should express conversion to and from the
+platform class differently than Kotlin classes. For Java classes, conversion
+from the platform class to the wrapper should be expressed as a `static` method,
+while conversion from the wrapper to the platform class should be a method on
+the wrapper object:
+
+```java
+@NonNull
+public static ModemInfoCompat toModemInfoCompat(@NonNull ModemInfo info);
+
+@NonNull
+public ModemInfo toModemInfo();
+```
+
+In cases where the primary library is written in Java and has an accompanying
+`-ktx` Kotlin extensions library, the following conversion should be provided as
+an extension function:
+
+```kotlin
+fun ModemInfo.toModemInfoCompat() : ModemInfoCompat
+```
+
+Whereas in cases where the primary library is written in Kotlin, the conversion
+should be provided as an extension factory:
+
+```kotlin
+class ModemInfoCompat {
+  fun toModemInfo() : ModemInfo
+
+  companion object {
+    @JvmStatic
+    @JvmName("toModemInfoCompat")
+    fun ModemInfo.toModemInfoCompat() : ModemInfoCompat
+  }
+}
+```
+
+#### API guidelines {#wrapper-api-guidelines}
+
+##### Naming {#wrapper-naming}
+
+*   Class name **must** be `<PlatformClass>Compat`
+*   Package name **must** be `androidx.core.<platform.package>`
+*   Superclass **must not** be `<PlatformClass>`
+
+##### Construction {#wrapper-construction}
+
+*   Class _may_ have public constructor(s) to provide parity with public
+    `PlatformClass` constructors
+    *   Constructor used to wrap `PlatformClass` **must not** be public
+*   Class **must** implement a static `PlatformClassCompat
+    toPlatformClassCompat(PlatformClass)` method to wrap `PlatformClass` on
+    supported SDK levels
+    *   If class does not exist at module's `minSdkVersion`, method must be
+        annotated with `@RequiresApi(<sdk>)` for SDK version where class was
+        introduced
+
+#### Implementation {#wrapper-implementation}
+
+*   Class **must** implement a `PlatformClass toPlatformClass()` method to
+    unwrap `PlatformClass` on supported SDK levels
+    *   If class does not exist at module's `minSdkVersion`, method must be
+        annotated with `@RequiresApi(<sdk>)` for SDK version where class was
+        introduced
+*   Implementation _may_ delegate to `PlatformClass` methods when available (see
+    below note for caveats)
+*   To avoid runtime class verification issues, all operations that interact
+    with the internal structure of `PlatformClass` must be implemented in inner
+    classes targeted to the SDK level at which the operation was added.
+    *   See the [sample](#wrapper-sample) for an example of interacting with a
+        method that was added in SDK level 23.
+
+### Standalone (ex. [ArraySet](https://developer.android.com/reference/android/support/v4/util/ArraySet.html), [Fragment](https://developer.android.com/reference/android/support/v4/app/Fragment.html)) {#standalone}
+
+When to use?
+
+*   Platform class may exist at module's `minSdkVersion`
+*   Does not need to integrate with platform APIs
+*   Does not need to coexist with platform class, ex. no potential `import`
+    collision due to both compatibility and platform classes being referenced
+    within the same source file
+
+Implementation requirements
+
+*   Class name **must** be `<PlatformClass>`
+*   Package name **must** be `androidx.<platform.package>`
+*   Superclass **must not** be `<PlatformClass>`
+*   Class **must not** expose `PlatformClass` in public API
+*   Implementation _may_ delegate to `PlatformClass` methods when available
+
+### Standalone JAR library (no Android dependencies) {#standalone-jar-library-no-android-dependencies}
+
+When to use
+
+*   General purpose library with minimal interaction with Android types
+    *   or when abstraction around types can be used (e.g. Room's SQLite
+        wrapper)
+*   Lib used in parts of app with minimal Android dependencies
+    *   ex. Repository, ViewModel
+*   When Android dependency can sit on top of common library
+*   Clear separation between android dependent and independent parts of your
+    library
+*   Clear that future integration with android dependencies can be layered
+    separately
+
+**Examples:**
+
+The **Paging Library** pages data from DataSources (such as DB content from Room
+or network content from Retrofit) into PagedLists, so they can be presented in a
+RecyclerView. Since the included Adapter receives a PagedList, and there are no
+other Android dependencies, Paging is split into two parts - a no-android
+library (paging-common) with the majority of the paging code, and an android
+library (paging-runtime) with just the code to present a PagedList in a
+RecyclerView Adapter. This way, tests of Repositories and their components can
+be tested in host-side tests.
+
+**Room** loads SQLite data on Android, but provides an abstraction for those
+that want to use a different SQL implementation on device. This abstraction, and
+the fact that Room generates code dynamically, means that Room interfaces can be
+used in host-side tests (though actual DB code should be tested on device, since
+DB impls may be significantly different on host).
+
+## Implementing compatibility {#compat}
+
+### Referencing new APIs {#compat-newapi}
+
+Generally, methods on extension library classes should be available to all
+devices above the library's `minSdkVersion`.
+
+#### Checking device SDK version {#compat-sdk}
+
+The most common way of delegating to platform or backport implementations is to
+compare the device's `Build.VERSION.SDK_INT` field to a known-good SDK version;
+for example, the SDK in which a method first appeared or in which a critical bug
+was first fixed.
+
+Non-reflective calls to new APIs gated on `SDK_INT` **must** be made from
+version-specific static inner classes to avoid verification errors that
+negatively affect run-time performance. For more information, see Chromium's
+guide to
+[Class Verification Failures](https://chromium.googlesource.com/chromium/src/+/HEAD/build/android/docs/class_verification_failures.md).
+
+Methods in implementation-specific classes **must** be paired with the
+`@DoNotInline` annotation to prevent them from being inlined.
+
+```java {.good}
+public static void saveAttributeDataForStyleable(@NonNull View view, ...) {
+  if (Build.VERSION.SDK_INT >= 29) {
+    Api29Impl.saveAttributeDataForStyleable(view, ...);
+  }
+}
+
+@RequiresApi(29)
+private static class Api29Impl {
+  @DoNotInline
+  static void saveAttributeDataForStyleable(@NonNull View view, ...) {
+    view.saveAttributeDataForStyleable(...);
+  }
+}
+```
+
+Alternatively, in Kotlin sources:
+
+```kotlin {.good}
+@RequiresApi(29)
+object Api25 {
+  @DoNotInline
+  fun saveAttributeDataForStyleable(view: View, ...) { ... }
+}
+```
+
+When developing against pre-release SDKs where the `SDK_INT` has not been
+finalized, SDK checks **must** use `BuildCompat.isAtLeastX()` methods.
+
+```java {.good}
+@NonNull
+public static List<Window> getAllWindows() {
+  if (BuildCompat.isAtLeastR()) {
+    return ApiRImpl.getAllWindows();
+  }
+  return Collections.emptyList();
+}
+```
+
+#### Device-specific issues {#compat-oem}
+
+Library code may work around device- or manufacturer-specific issues -- issues
+not present in AOSP builds of Android -- *only* if a corresponding CTS test
+and/or CDD policy is added to the next revision of the Android platform. Doing
+so ensures that such issues can be detected and fixed by OEMs.
+
+#### Handling `minSdkVersion` disparity {#compat-minsdk}
+
+Methods that only need to be accessible on newer devices, including
+`to<PlatformClass>()` methods, may be annotated with `@RequiresApi(<sdk>)` to
+indicate they will fail to link on older SDKs. This annotation is enforced at
+build time by Lint.
+
+#### Handling `targetSdkVersion` behavior changes {#compat-targetsdk}
+
+To preserve application functionality, device behavior at a given API level may
+change based on an application's `targetSdkVersion`. For example, if an app with
+`targetSdkVersion` set to API level 22 runs on a device with API level 29, all
+required permissions will be granted at installation time and the run-time
+permissions framework will emulate earlier device behavior.
+
+Libraries do not have control over the app's `targetSdkVersion` and -- in rare
+cases -- may need to handle variations in platform behavior. Refer to the
+following pages for version-specific behavior changes:
+
+*   API level 29:
+    [Android Q behavior changes: apps targeting Q](https://developer.android.com/preview/behavior-changes-q)
+*   API level 28:
+    [Behavior changes: apps targeting API level 28+](https://developer.android.com/about/versions/pie/android-9.0-changes-28)
+*   API level 26:
+    [Changes for apps targeting Android 8.0](https://developer.android.com/about/versions/oreo/android-8.0-changes#o-apps)
+*   API level 24:
+    [Changes for apps targeting Android 7.0](https://developer.android.com/about/versions/nougat/android-7.0-changes#n-apps)
+*   API level 21:
+    [Android 5.0 Behavior Changes](https://developer.android.com/about/versions/android-5.0-changes)
+*   API level 19:
+    [Android 4.4 APIs](https://developer.android.com/about/versions/android-4.4)
+
+#### Working around Lint issues {#compat-lint}
+
+In rare cases, Lint may fail to interpret API usages and yield a `NewApi` error
+and require the use of `@TargetApi` or `@SuppressLint('NewApi')` annotations.
+Both of these annotations are strongly discouraged and may only be used
+temporarily. They **must never** be used in a stable release. Any usage of these
+annotation **must** be associated with an active bug, and the usage must be
+removed when the bug is resolved.
+
+### Delegating to API-specific implementations {#delegating-to-api-specific-implementations}
+
+#### SDK-dependent reflection
+
+Starting in API level 28, the platform restricts which
+[non-SDK interfaces](https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces)
+can be accessed via reflection by apps and libraries. As a general rule, you
+will **not** be able to use reflection to access hidden APIs on devices with
+`SDK_INT` greater than `Build.VERSION_CODES.P` (28).
+
+On earlier devices, reflection on hidden platform APIs is allowed **only** when
+an alternative public platform API exists in a later revision of the Android
+SDK. For example, the following implementation is allowed:
+
+```java
+public AccessibilityDelegate getAccessibilityDelegate(View v) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+        // Retrieve the delegate using a public API.
+        return v.getAccessibilityDelegate();
+    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+        // Retrieve the delegate by reflecting on a private field. If the
+        // field does not exist or cannot be accessed, this will no-op.
+        if (sAccessibilityDelegateField == null) {
+            try {
+                sAccessibilityDelegateField = View.class
+                        .getDeclaredField("mAccessibilityDelegate");
+                sAccessibilityDelegateField.setAccessible(true);
+            } catch (Throwable t) {
+                sAccessibilityDelegateCheckFailed = true;
+                return null;
+            }
+        }
+        try {
+            Object o = sAccessibilityDelegateField.get(v);
+            if (o instanceof View.AccessibilityDelegate) {
+                return (View.AccessibilityDelegate) o;
+            }
+            return null;
+        } catch (Throwable t) {
+            sAccessibilityDelegateCheckFailed = true;
+            return null;
+        }
+    } else {
+        // There is no way to retrieve the delegate, even via reflection.
+        return null;
+    }
+```
+
+Calls to public APIs added in pre-release revisions *must* be gated using
+`BuildCompat`:
+
+```java
+if (BuildCompat.isAtLeastQ()) {
+   // call new API added in Q
+} else if (Build.SDK_INT.VERSION >= Build.VERSION_CODES.SOME_RELEASE) {
+   // make a best-effort using APIs that we expect to be available
+} else {
+   // no-op or best-effort given no information
+}
+```
+
+### Inter-process communication {#inter-process-communication}
+
+Protocols and data structures used for IPC must support interoperability between
+different versions of libraries and should be treated similarly to public API.
+
+#### Data structures
+
+**Do not** use Parcelable for any class that may be used for IPC or otherwise
+exposed as public API. The data format used by Parcelable does not provide any
+compatibility guarantees and will result in crashes if fields are added or
+removed between library versions.
+
+**Do not** design your own serialization mechanism or wire format for disk
+storage or inter-process communication. Preserving and verifying compatibility
+is difficult and error-prone.
+
+If you expose a `Bundle` to callers that can cross processes, you should
+[prevent apps from adding their own custom parcelables](https://android.googlesource.com/platform/frameworks/base/+/6cddbe14e1ff67dc4691a013fe38a2eb0893fe03)
+as top-level entries; if *any* entry in a `Bundle` can't be loaded, even if it's
+not actually accessed, the receiving process is likely to crash.
+
+**Do** use protocol buffers or, in some simpler cases, `VersionedParcelable`.
+
+#### Communication protocols
+
+Any communication prototcol, handshake, etc. must maintain compatibility
+consistent with SemVer guidelines. Consider how your protocol will handle
+addition and removal of operations or constants, compatibility-breaking changes,
+and other modifications without crashing either the host or client process.
+
+## Deprecation and removal
+
+While SemVer's binary compatibility guarantees restrict the types of changes
+that may be made within a library revision and make it difficult to remove an
+API, there are many other ways to influence how developers interact with your
+library.
+
+### Deprecation (`@deprecated`)
+
+Deprecation lets a developer know that they should stop using an API or class.
+All deprecations must be marked with a `@Deprecated` Java annotation as well as
+a `@deprecated <migration-docs>` docs annotation explaining how the developer
+should migrate away from the API.
+
+Deprecation is an non-breaking API change that must occur in a **major** or
+**minor** release.
+
+### Soft removal (@removed)
+
+Soft removal preserves binary compatibility while preventing source code from
+compiling against an API. It is a *source-breaking change* and not recommended.
+
+Soft removals **must** do the following:
+
+*   Mark the API as deprecated for at least one stable release prior to removal.
+*   Mark the API with a `@RestrictTo(LIBRARY)` Java annotation as well as a
+    `@removed <reason>` docs annotation explaining why the API was removed.
+*   Maintain binary compatibility, as the API may still be called by existing
+    dependent libraries.
+*   Maintain behavioral compatibility and existing tests.
+
+This is a disruptive change and should be avoided when possible.
+
+Soft removal is a source-breaking API change that must occur in a **major** or
+**minor** release.
+
+### Hard removal
+
+Hard removal entails removing the entire implementation of an API that was
+exposed in a public release. Prior to removal, an API must be marked as
+`@deprecated` for a full **minor** version (`alpha`->`beta`->`rc`->stable),
+prior to being hard removed.
+
+This is a disruptive change and should be avoided when possible.
+
+Hard removal is a binary-breaking API change that must occur in a **major**
+release.
+
+### For entire artifacts
+
+We do not typically deprecate or remove entire artifacts; however, it may be
+useful in cases where we want to halt development and focus elsewhere or
+strongly discourage developers from using a library.
+
+Halting development, either because of staffing or prioritization issues, leaves
+the door open for future bug fixes or continued development. This quite simply
+means we stop releasing updates but retain the source in our tree.
+
+Deprecating an artifact provides developers with a migration path and strongly
+encourages them -- through Lint warnings -- to migrate elsewhere. This is
+accomplished by adding a `@Deprecated` and `@deprecated` (with migration
+comment) annotation pair to *every* class and interface in the artifact.
+
+The fully-deprecated artifact will be released as a deprecation release -- it
+will ship normally with accompanying release notes indicating the reason for
+deprecation and migration strategy, and it will be the last version of the
+artifact that ships. It will ship as a new minor stable release. For example, if
+`1.0.0` was the last stable release, then the deprecation release will be
+`1.1.0`. This is so Android Studio users will get a suggestion to update to a
+new stable version, which will contain the `@deprecated` annotations.
+
+After an artifact has been released as fully-deprecated, it can be removed from
+the source tree.
+
+## Resources {#resources}
+
+Generally, follow the official Android guidelines for
+[app resources](https://developer.android.com/guide/topics/resources/providing-resources).
+Special guidelines for library resources are noted below.
+
+### Defining new resources
+
+Libraries may define new value and attribute resources using the standard
+application directory structure used by Android Gradle Plugin:
+
+```
+src/main/res/
+  values/
+    attrs.xml   Theme attributes and styleables
+    dimens.xml  Dimensional values
+    public.xml  Public resource definitions
+    ...
+```
+
+However, some libraries may still be using non-standard, legacy directory
+structures such as `res-public` for their public resource declarations or a
+top-level `res` directory and accompanying custom source set in `build.gradle`.
+These libraries will eventually be migrated to follow standard guidelines.
+
+#### Naming conventions
+
+Libraries follow the Android platform's resource naming conventions, which use
+`camelCase` for attributes and `underline_delimited` for values. For example,
+`R.attr.fontProviderPackage` and `R.dimen.material_blue_grey_900`.
+
+#### Attribute formats
+
+At build time, attribute definitions are pooled globally across all libraries
+used in an application, which means attribute `format`s *must* be identical for
+a given `name` to avoid a conflict.
+
+Within Jetpack, new attribute names *must* be globally unique. Libraries *may*
+reference existing public attributes from their dependencies. See below for more
+information on public attributes.
+
+When adding a new attribute, the format should be defined *once* in an `<attr
+/>` element in the definitions block at the top of `src/main/res/attrs.xml`.
+Subsequent references in `<declare-styleable>` elements *must* not include a
+`format`:
+
+`src/main/res/attrs.xml`
+
+```xml
+<resources>
+  <attr name="fontProviderPackage" format="string" />
+
+  <declare-styleable name="FontFamily">
+      <attr name="fontProviderPackage" />
+  </declare-styleable>
+</resources>
+```
+
+### Public resources
+
+Library resources are private by default, which means developers are discouraged
+from referencing any defined attributes or values from XML or code; however,
+library resources may be declared public to make them available to developers.
+
+Public library resources are considered API surface and are thus subject to the
+same API consistency and documentation requirements as Java APIs.
+
+Libraries will typically only expose theme attributes, ex. `<attr />` elements,
+as public API so that developers can set and retrieve the values stored in
+styles and themes. Exposing values -- such as `<dimen />` and `<string />` -- or
+images -- such as drawable XML and PNGs -- locks the current state of those
+elements as public API that cannot be changed without a major version bump. That
+means changing a publicly-visible icon would be considered a breaking change.
+
+#### Documentation
+
+All public resource definitions should be documented, including top-level
+definitions and re-uses inside `<styleable>` elements:
+
+`src/main/res/attrs.xml`
+
+```xml
+<resources>
+  <!-- String specifying the application package for a Font Provider. -->
+  <attr name="fontProviderPackage" format="string" />
+
+  <!-- Attributes that are read when parsing a <fontfamily> tag. -->
+  <declare-styleable name="FontFamily">
+      <!-- The package for the Font Provider to be used for the request. This is
+           used to verify the identity of the provider. -->
+      <attr name="fontProviderPackage" />
+  </declare-styleable>
+</resources>
+```
+
+`src/main/res/colors.xml`
+
+```xml
+<resources>
+  <!-- Color for Material Blue-Grey 900. -->
+  <color name="material_blue_grey_900">#ff263238</color>
+</resources>
+```
+
+#### Public declaration
+
+Resources are declared public by providing a separate `<public />` element with
+a matching type:
+
+`src/main/res/public.xml`
+
+```xml
+<resources>
+  <public name="fontProviderPackage" type="attr" />
+  <public name="material_blue_grey_900" type="color" />
+</resources>
+```
+
+#### More information
+
+See also the official Android Gradle Plugin documentation for
+[Private Resources](https://developer.android.com/studio/projects/android-library#PrivateResources).
+
+### Manifest entries (`AndroidManifest.xml`) {#resources-manifest}
+
+#### Metadata tags (`<meta-data>`) {#resources-manifest-metadata}
+
+Developers **must not** add `<application>`-level `<meta-data>` tags to library
+manifests or advise developers to add such tags to their application manifests.
+Doing so may _inadvertently cause denial-of-service attacks against other apps_.
+
+Assume a library adds a single item of meta-data at the application level. When
+an app uses the library, that meta-data will be merged into the resulting app's
+application entry via manifest merger.
+
+If another app attempts to obtain a list of all activities associated with the
+primary app, that list will contain multiple copies of the `ApplicationInfo`,
+each of which in turn contains a copy of the library's meta-data. As a result,
+one `<metadata>` tag may become hundreds of KB on the binder call to obtain the
+list -- resulting in apps hitting transaction too large exceptions and crashing.
+
+```xml {.bad}
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.librarypackage">
+  <application>
+    <meta-data
+        android:name="keyName"
+        android:value="@string/value" />
+  </application>
+</manifest>
+```
+
+Instead, developers may consider adding `<metadata>` nested inside of
+placeholder `<service>` tags.
+
+```xml {.good}
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.librarypackage">
+  <application>
+    <service
+        android:name="androidx.librarypackage.MetadataHolderService"
+        android:enabled="false"
+        android:exported="false">
+      <meta-data
+          android:name="androidx.librarypackage.MetadataHolderService.KEY_NAME"
+          android:resource="@string/value" />
+    </service>
+  </application>
+```
+
+```java {.good}
+package androidx.libraryname.featurename;
+
+/**
+ * A placeholder service to avoid adding application-level metadata. The service
+ * is only used to expose metadata defined in the library's manifest. It is
+ * never invoked.
+ */
+public final class MetadataHolderService {
+  private MetadataHolderService() {}
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    throw new UnsupportedOperationException();
+  }
+}
+```
+
+## Dependencies {#dependencies}
+
+Generally, Jetpack libraries should avoid dependencies that negatively impact
+developers without providing substantial benefit. This includes large
+dependencies where only a small portion is needed, dependencies that slow down
+build times through annotation processing or compiler overhead, and generally
+any dependency that negatively affects system health.
+
+### Kotlin {#dependencies-kotlin}
+
+Kotlin is _recommended_ for new libraries; however, it's important to consider
+its size impact on clients. Currently, the Kotlin stdlib adds a minimum of 40kB
+post-optimization.
+
+### Kotlin coroutines {#dependencies-coroutines}
+
+Kotlin's coroutine library adds around 100kB post-shrinking. New libraries that
+are written in Kotlin should prefer coroutines over `ListenableFuture`, but
+existing libraries must consider the size impact on their clients. See
+[Asynchronous work with return values](#async-return) for more details on using
+Kotlin coroutines in Jetpack libraries.
+
+### Guava {#dependencies-guava}
+
+The full Guava library is very large and *must not* be used. Libraries that
+would like to depend on Guava's `ListenableFuture` may instead depend on the
+standalone `com.google.guava:listenablefuture` artifact. See
+[Asynchronous work with return values](#async-return) for more details on using
+`ListenableFuture` in Jetpack libraries.
+
+### Java 8 {#dependencies-java8}
+
+Libraries that take a dependency on a library targeting Java 8 must _also_
+target Java 8, which will incur a ~5% build performance (as of 8/2019) hit for
+clients. New libraries targeting Java 8 may use Java 8 dependencies; however,
+existing libraries targeting Java 7 should not.
+
+The default language level for `androidx` libraries is Java 8, and we encourage
+libraries to stay on Java 8. However, if you have a business need to target Java
+7, you can specify Java 7 in your `build.gradle` as follows:
+
+```Groovy
+android {
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_7
+        targetCompatibility = JavaVersion.VERSION_1_7
+    }
+}
+```
+
+## More API guidelines {#more-api-guidelines}
+
+### Annotations {#annotation}
+
+#### Annotation processors {#annotation-processor}
+
+Annotation processors should opt-in to incremental annotation processing to
+avoid triggering a full recompilation on every client source code change. See
+Gradle's
+[Incremental annotation processing](https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing)
+documentation for information on how to opt-in.
+
+### Experimental APIs {#experimental-api}
+
+Jetpack libraries may choose to annotate API surfaces as unstable using either
+Kotlin's
+[`@Experimental` annotation](https://kotlinlang.org/docs/reference/experimental.html)
+for APIs written in Kotlin or Jetpack's
+[`@Experimental` annotation](https://developer.android.com/reference/kotlin/androidx/annotation/experimental/Experimental)
+for APIs written in Java.
+
+In both cases, API surfaces marked as experimental are considered alpha and will
+be excluded from API compatibility guarantees. Due to the lack of compatibility
+guarantees, libraries *must never* call experimental APIs exposed by other
+libraries and *may not* use the `@UseExperimental` annotation except in the
+following cases:
+
+*   A library within a same-version group *may* call an experimental API exposed
+    by another library **within its same-version group**. In this case, API
+    compatibility guarantees are covered under the same-version group policies
+    and the library *may* use the `@UsesExperimental` annotation to prevent
+    propagation of the experimental property. **Library owners must exercise
+    care to ensure that post-alpha APIs backed by experimental APIs actually
+    meet the release criteria for post-alpha APIs.**
+
+#### How to mark an API surface as experimental
+
+All libraries using `@Experimental` annotations *must* depend on the
+`androidx.annotation:annotation-experimental` artifact regardless of whether
+they are using the `androidx` or Kotlin annotation. This artifact provides Lint
+enforcement of experimental usage restrictions for Kotlin callers as well as
+Java (which the Kotlin annotation doesn't handle on its own, since it's a Kotlin
+compiler feature). Libraries *may* include the dependency as `api`-type to make
+`@UseExperimental` available to Java clients; however, this will also
+unnecessarily expose the `@Experimental` annotation.
+
+```java
+dependencies {
+    implementation(project(":annotation:annotation-experimental"))
+}
+```
+
+See Kotlin's
+[experimental marker documentation](https://kotlinlang.org/docs/reference/experimental.html)
+for general usage information. If you are writing experimental Java APIs, you
+will use the Jetpack
+[`@Experimental` annotation](https://developer.android.com/reference/kotlin/androidx/annotation/experimental/Experimental)
+rather than the Kotlin compiler's annotation.
+
+#### How to transition an API out of experimental
+
+When an API surface is ready to transition out of experimental, the annotation
+may only be removed during an alpha pre-release stage since removing the
+experimental marker from an API is equivalent to adding the API to the current
+API surface.
+
+When transitioning an entire feature surface out of experimental, you *should*
+remove the associated annotations.
+
+When making any change to the experimental API surface, you *must* run
+`./gradlew updateApi` prior to uploading your change.
+
+### Restricted APIs {#restricted-api}
+
+Jetpack's library tooling supports hiding Java-visible (ex. `public` and
+`protected`) APIs from developers using a combination of the `@hide` docs
+annotation and `@RestrictTo` source annotation. These annotations **must** be
+paired together when used, and are validated as part of presubmit checks for
+Java code (Kotlin not yet supported by Checkstyle).
+
+The effects of hiding an API are as follows:
+
+*   The API will not appear in documentation
+*   Android Studio will warn the developer not to use the API
+
+Hiding an API does *not* provide strong guarantees about usage:
+
+*   There are no runtime restrictions on calling hidden APIs
+*   Android Studio will not warn if hidden APIs are called using reflection
+*   Hidden APIs will still show in Android Studio's auto-complete
+
+#### When to use `@hide` {#restricted-api-usage}
+
+Generally, avoid using `@hide`. The `@hide` annotation indicates that developers
+should not call an API that is _technically_ public from a Java visibility
+perspective. Hiding APIs is often a sign of a poorly-abstracted API surface, and
+priority should be given to creating public, maintainable APIs and using Java
+visibility modifiers.
+
+*Do not* use `@hide` to bypass API tracking and review for production APIs;
+instead, rely on API+1 and API Council review to ensure APIs are reviewed on a
+timely basis.
+
+*Do not* use `@hide` for implementation detail APIs that are used between
+libraries and could reasonably be made public.
+
+*Do* use `@hide` paired with `@RestrictTo(LIBRARY)` for implementation detail
+APIs used within a single library (but prefer Java language `private` or
+`default` visibility).
+
+#### `RestrictTo.Scope` and inter- versus intra-library API surfaces {#private-api-types}
+
+To maintain binary compatibility between different versions of libraries,
+restricted API surfaces that are used between libraries (inter-library APIs)
+must follow the same Semantic Versioning rules as public APIs. Inter-library
+APIs should be annotated with the `@RestrictTo(LIBRARY_GROUP)` source
+annotation.
+
+Restricted API surfaces used within a single library (intra-library APIs), on
+the other hand, may be added or removed without any compatibility
+considerations. It is safe to assume that developers _never_ call these APIs,
+even though it is technically feasible. Intra-library APIs should be annotated
+with the `@RestrictTo(LIBRARY)` source annotation.
+
+The following table shows the visibility of a hypothetical API within Maven
+coordinate `androidx.concurrent:concurrent` when annotated with a variety of
+scopes:
+
+<table>
+    <tr>
+        <td><code>RestrictTo.Scope</code></td>
+        <td>Visibility by Maven coordinate</td>
+    </tr>
+    <tr>
+        <td><code>LIBRARY</code></td>
+        <td><code>androidx.concurrent:concurrent</code></td>
+    </tr>
+    <tr>
+        <td><code>LIBRARY_GROUP</code></td>
+        <td><code>androidx.concurrent:*</code></td>
+    </tr>
+    <tr>
+        <td><code>LIBRARY_GROUP_PREFIX</code></td>
+        <td><code>androidx.*:*</code></td>
+    </tr>
+</table>
+
+### Constructors {#constructors}
+
+#### View constructors {#view-constructors}
+
+The four-arg View constructor -- `View(Context, AttributeSet, int, int)` -- was
+added in SDK 21 and allows a developer to pass in an explicit default style
+resource rather than relying on a theme attribute to resolve the default style
+resource. Because this API was added in SDK 21, care must be taken to ensure
+that it is not called through any < SDK 21 code path.
+
+Views _may_ implement a four-arg constructor in one of the following ways:
+
+1.  Do not implement.
+1.  Implement and annotate with `@RequiresApi(21)`. This means the three-arg
+    constructor **must not** call into the four-arg constructor.
+
+### Asynchronous work {#async}
+
+#### With return values {#async-return}
+
+Traditionally, asynchronous work on Android that results in an output value
+would use a callback; however, better alternatives exist for libraries.
+
+Kotlin libraries should prefer
+[coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) and
+`suspend` functions, but please refer to the guidance on
+[allowable dependencies](#dependencies-coroutines) before adding a new
+dependency on coroutines.
+
+Java libraries should prefer `ListenableFuture` and the
+[`CallbackToFutureAdapter`](https://developer.android.com/reference/androidx/concurrent/futures/CallbackToFutureAdapter)
+implementation provided by the `androidx.concurrent:concurrent-futures` library.
+
+Libraries **must not** use `java.util.concurrent.CompletableFuture`, as it has a
+large API surface that permits arbitrary mutation of the future's value and has
+error-prone defaults.
+
+See the [Dependencies](#dependencies) section for more information on using
+Kotlin coroutines and Guava in your library.
+
+#### Avoid `synchronized` methods
+
+Whenever multiple threads are interacting with shared (mutable) references those
+reads and writes must be synchronized in some way. However synchronized blocks
+make your code thread-safe at the expense of concurrent execution. Any time
+execution enters a synchronized block or method any other thread trying to enter
+a synchronized block on the same object has to wait; even if in practice the
+operations are unrelated (e.g. they interact with different fields). This can
+dramatically reduce the benefit of trying to write multi-threaded code in the
+first place.
+
+Locking with synchronized is a heavyweight form of ensuring ordering between
+threads, and there are a number of common APIs and patterns that you can use
+that are more lightweight, depending on your use case:
+
+*   Compute a value once and make it available to all threads
+*   Update Set and Map data structures across threads
+*   Allow a group of threads to process a stream of data concurrently
+*   Provide instances of a non-thread-safe type to multiple threads
+*   Update a value from multiple threads atomically
+*   Maintain granular control of your concurrency invariants
+
+### Kotlin {#kotlin}
+
+#### Data classes {#kotlin-data}
+
+Kotlin `data` classes provide a convenient way to define simple container
+objects, where Kotlin will generate `equals()` and `hashCode()` for you.
+However, they are not designed to preserve API/binary compatibility when members
+are added. This is due to other methods which are generated for you -
+[destructuring declarations](https://kotlinlang.org/docs/reference/multi-declarations.html),
+and [copying](https://kotlinlang.org/docs/reference/data-classes.html#copying).
+
+Example data class as tracked by metalava:
+
+<pre>
+  public final class TargetAnimation {
+    ctor public TargetAnimation(float target, androidx.animation.AnimationBuilder animation);
+    <b>method public float component1();</b>
+    <b>method public androidx.animation.AnimationBuilder component2();</b>
+    <b>method public androidx.animation.TargetAnimation copy(float target, androidx.animation.AnimationBuilder animation);</b>
+    method public androidx.animation.AnimationBuilder getAnimation();
+    method public float getTarget();
+  }
+</pre>
+
+Because members are exposed as numbered components for destructuring, you can
+only safely add members at the end of the member list. As `copy` is generated
+with every member name in order as well, you'll also have to manually
+re-implement any old `copy` variants as items are added. If these constraints
+are acceptable, data classes may still be useful to you.
+
+As a result, Kotlin `data` classes are _strongly discouraged_ in library APIs.
+Instead, follow best-practices for Java data classes including implementing
+`equals`, `hashCode`, and `toString`.
+
+See Jake Wharton's article on
+[Public API challenges in Kotlin](https://jakewharton.com/public-api-challenges-in-kotlin/)
+for more details.
+
+#### Extension and top-level functions {#kotlin-extension-functions}
+
+If your Kotlin file contains any sybmols outside of class-like types
+(extension/top-level functions, properties, etc), the file must be annotated
+with `@JvmName`. This ensures unanticipated use-cases from Java callers don't
+get stuck using `BlahKt` files.
+
+Example:
+
+```kotlin {.bad}
+package androidx.example
+
+fun String.foo() = // ...
+```
+
+```kotlin {.good}
+@file:JvmName("StringUtils")
+
+package androidx.example
+
+fun String.foo() = // ...
+```
+
+NOTE This guideline may be ignored for libraries that only work in Kotlin (think
+Compose).
+
+## Testing Guidelines
+
+### [Do not Mock, AndroidX](do_not_mock.md)
+
+## Android Lint Guidelines
+
+### Suppression vs Baselines
+
+Lint sometimes flags false positives, even though it is safe to ignore these
+errors (for example WeakerAccess warnings when you are avoiding synthetic
+access). There may also be lint failures when your library is in the middle of a
+beta / rc / stable release, and cannot make the breaking changes needed to fix
+the root cause. There are two ways of ignoring lint errors:
+
+1.  Suppression - using `@SuppressLint` (for Java) or `@Suppress` annotations to
+    ignore the warning per call site, per method, or per file. *Note
+    `@SuppressLint` - Requires Android dependency*.
+2.  Baselines - allowlisting errors in a lint-baseline.xml file at the root of
+    the project directory.
+
+Where possible, you should use a **suppression annotation at the call site**.
+This helps ensure that you are only suppressing the *exact* failure, and this
+also keeps the failure visible so it can be fixed later on. Only use a baseline
+if you are in a Java library without Android dependencies, or when enabling a
+new lint check, and it is prohibitively expensive / not possible to fix the
+errors generated by enabling this lint check.
+
+To update a lint baseline (lint-baseline.xml) after you have fixed issues, add
+`-PupdateLintBaseline` to the end of your lint command. This will delete and
+then regenerate the baseline file.
+
+```shell
+./gradlew core:lintDebug -PupdateLintBaseline
+```
+
+## Metalava API Lint
+
+As well as Android Lint, which runs on all source code, Metalava will also run
+checks on the public API surface of each library. Similar to with Android Lint,
+there can sometimes be false positives / intended deviations from the API
+guidelines that Metalava will lint your API surface against. When this happens,
+you can suppress Metalava API lint issues using `@SuppressLint` (for Java) or
+`@Suppress` annotations. In cases where it is not possible, update Metalava's
+baseline with the `updateApiLintBaseline` task.
+
+```shell
+./gradlew core:updateApiLintBaseline
+```
+
+This will create/amend the `api_lint.ignore` file that lives in a library's
+`api` directory.
+
+## Build Output Guidelines
+
+In order to more easily identify the root cause of build failures, we want to
+keep the amount of output generated by a successful build to a minimum.
+Consequently, we track build output similarly to the way in which we track Lint
+warnings.
+
+### Invoking build output validation
+
+You can add `-Pandroidx.validateNoUnrecognizedMessages` to any other AndroidX
+gradlew command to enable validation of build output. For example:
+
+```shell
+/gradlew -Pandroidx.validateNoUnrecognizedMessages :help
+```
+
+### Exempting new build output messages
+
+Please avoid exempting new build output and instead fix or suppress the warnings
+themselves, because that will take effect not only on the build server but also
+in Android Studio, and will also run more quickly.
+
+If you cannot prevent the message from being generating and must exempt the
+message anyway, follow the instructions in the error:
+
+```shell
+$ ./gradlew -Pandroidx.validateNoUnrecognizedMessages :help
+
+Error: build_log_simplifier.py found 15 new messages found in /usr/local/google/workspace/aosp-androidx-git/out/dist/gradle.log.
+
+Please fix or suppress these new messages in the tool that generates them.
+If you cannot, then you can exempt them by doing:
+
+  1. cp /usr/local/google/workspace/aosp-androidx-git/out/dist/gradle.log.ignore /usr/local/google/workspace/aosp-androidx-git/frameworks/support/development/build_log_simplifier/messages.ignore
+  2. modify the new lines to be appropriately generalized
+```
+
+Each line in this exemptions file is a regular expressing matching one or more
+lines of output to be exempted. You may want to make these expressions as
+specific as possible to ensure that the addition of new, similar messages will
+also be detected (for example, discovering an existing warning in a new source
+file).
+
+## Behavior changes
+
+### Changes that affect API documentation
+
+Do not make behavior changes that require altering API documentation in a way
+that would break existing clients, even if such changes are technically binary
+compatible. For example, changing the meaning of a method's return value to
+return true rather than false in a given state would be considered a breaking
+change. Because this change is binary-compatible, it will not be caught by
+tooling and is effectively invisible to clients.
+
+Instead, add new methods and deprecate the existing ones if necessary, noting
+behavior changes in the deprecation message.
+
+### High-risk behavior changes
+
+Behavior changes that conform to documented API contracts but are highly complex
+and difficult to comprehensively test are considered high-risk and should be
+implemented using behavior flags. These changes may be flagged on initially, but
+the original behaviors must be preserved until the library enters release
+candidate stage and the behavior changes have been appropriately verified by
+integration testing against public pre-release
+revisions.
+
+It may be necessary to soft-revert a high-risk behavior change with only 24-hour
+notice, which should be achievable by flipping the behavior flag to off.
+
+```java
+[example code pending]
+```
+
+Avoid adding multiple high-risk changes during a feature cycle, as verifying the
+interaction of multiple feature flags leads to unnecessary complexity and
+exposes clients to high risk even when a single change is flagged off. Instead,
+wait until one high-risk change has landed in RC before moving on to the next.
+
+#### Testing
+
+Relevant tests should be run for the behavior change in both the on and off
+flagged states to prevent regressions.
+
+## Sample code in Kotlin modules
+
+### Background
+
+Public API can (and should!) have small corresponding code snippets that
+demonstrate functionality and usage of a particular API. These are often exposed
+inline in the documentation for the function / class - this causes consistency
+and correctness issues as this code is not compiled against, and the underlying
+implementation can easily change.
+
+KDoc (JavaDoc for Kotlin) supports a `@sample` tag, which allows referencing the
+body of a function from documentation. This means that code samples can be just
+written as a normal function, compiled and linted against, and reused from other
+modules such as tests! This allows for some guarantees on the correctness of a
+sample, and ensuring that it is always kept up to date.
+
+### Enforcement
+
+There are still some visibility issues here - it can be hard to tell if a
+function is a sample, and is used from public documentation - so as a result we
+have lint checks to ensure sample correctness.
+
+Primarily, there are three requirements when using sample links:
+
+1.  All functions linked to from a `@sample` KDoc tag must be annotated with
+    `@Sampled`
+2.  All sample functions annotated with `@Sampled` must be linked to from a
+    `@sample` KDoc tag
+3.  All sample functions must live inside a separate `samples` library
+    submodule - see the section on module configuration below for more
+    information.
+
+This enforces visibility guarantees, and make it easier to know that a sample is
+a sample. This also prevents orphaned samples that aren't used, and remain
+unmaintained and outdated.
+
+### Sample usage
+
+The follow demonstrates how to reference sample functions from public API. It is
+also recommended to reuse these samples in unit tests / integration tests / test
+apps / library demos where possible.
+
+**Public API:**
+
+```
+/*
+ * Fancy prints the given [string]
+ *
+ * @sample androidx.printer.samples.fancySample
+ */
+fun fancyPrint(str: String) ...
+```
+
+**Sample function:**
+
+```
+package androidx.printer.samples
+
+import androidx.printer.fancyPrint
+
+@Sampled
+fun fancySample() {
+   fancyPrint("Fancy!")
+}
+```
+
+**Generated documentation visible on d.android.com\***
+
+```
+fun fancyPrint(str: String)
+
+Fancy prints the given [string]
+
+<code>
+ import androidx.printer.fancyPrint
+
+ fancyPrint("Fancy!")
+<code>
+```
+
+\**still some improvements to be made to DAC side, such as syntax highlighting*
+
+### Module configuration
+
+The following module setups should be used for sample functions, and are
+enforced by lint:
+
+**Group-level samples**
+
+For library groups with strongly related samples that want to share code.
+
+Gradle project name: `:foo-library:samples`
+
+```
+foo-library/
+  foo-module/
+  bar-module/
+  samples/
+```
+
+**Per-module samples**
+
+For library groups with complex, relatively independent sub-libraries
+
+Gradle project name: `:foo-library:foo-module:samples`
+
+```
+foo-library/
+  foo-module/
+    samples/
+```
diff --git a/docs/benchmarking.md b/docs/benchmarking.md
new file mode 100644
index 0000000..cc38f78
--- /dev/null
+++ b/docs/benchmarking.md
@@ -0,0 +1,273 @@
+# Benchmarking in AndroidX
+
+[TOC]
+
+The public documentation at
+[d.android.com/benchmark](http://d.android.com/benchmark) explains how to use
+the library - this page focuses on specifics to writing libraries in the
+AndroidX repo, and our continuous testing / triage process.
+
+### Writing the benchmark
+
+Benchmarks are just regular instrumentation tests! Just use the
+[`BenchmarkRule`](https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/benchmark/junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt)
+provided by the library:
+
+<section class="tabs">
+
+#### Kotlin {.new-tab}
+
+```kotlin
+@RunWith(AndroidJUnit4::class)
+class ViewBenchmark {
+    @get:Rule
+    val benchmarkRule = BenchmarkRule()
+
+    @Test
+    fun simpleViewInflate() {
+        val context = InstrumentationRegistry
+                .getInstrumentation().targetContext
+        val inflater = LayoutInflater.from(context)
+        val root = FrameLayout(context)
+
+        benchmarkRule.measure {
+            inflater.inflate(R.layout.test_simple_view, root, false)
+        }
+    }
+}
+```
+
+#### Java {.new-tab}
+
+```java
+@RunWith(AndroidJUnit4.class)
+public class ViewBenchmark {
+    @Rule
+    public BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+
+    @Test
+    public void simpleViewInflate() {
+        Context context = InstrumentationRegistry
+                .getInstrumentation().getTargetContext();
+        final BenchmarkState state = mBenchmarkRule.getState();
+        LayoutInflater inflater = LayoutInflater.from(context);
+        FrameLayout root = new FrameLayout(context);
+
+        while (state.keepRunning()) {
+            inflater.inflate(R.layout.test_simple_view, root, false);
+        }
+    }
+}
+```
+
+</section>
+
+## Project structure
+
+As in the public documentation, benchmarks in the AndroidX repo are test-only
+library modules. Differences for AndroidX repo:
+
+1.  Module name must end with `-benchmark` in `settings.gradle`.
+2.  You do not need to apply the benchmark plugin (it's pulled in automatically
+    from source)
+
+### I'm lazy and want to start quickly
+
+Start by copying one of the following projects:
+
+*   [navigation-benchmark](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/navigation/benchmark/)
+*   [recyclerview-benchmark](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/recyclerview/recyclerview-benchmark/)
+
+### Compose
+
+Compose builds the benchmark from source, so usage matches the rest of the
+AndroidX project. See existing Compose benchmark projects:
+
+*   [Compose UI benchmarks](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/ui/integration-tests/benchmark/)
+*   [Compose Runtime benchmarks](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/compose/compose-runtime/compose-runtime-benchmark/)
+
+## Profiling
+
+### Command Line
+
+The benchmark library supports capturing profiling information - sampled and
+method - from the command line. Here's an example which runs the
+`androidx.ui.benchmark.test.CheckboxesInRowsBenchmark#draw` method with
+`MethodSampling` profiling:
+
+```
+./gradlew compose:integ:bench:cC \
+    -P android.testInstrumentationRunnerArguments.androidx.benchmark.profiling.mode=MethodSampling \
+    -P android.testInstrumentationRunnerArguments.class=androidx.ui.benchmark.test.CheckboxesInRowsBenchmark#draw
+```
+
+The command output will tell you where to look for the file on your host
+machine:
+
+```
+04:33:49 I/Benchmark: Benchmark report files generated at
+/androidx-master-dev/out/ui/ui/integration-tests/benchmark/build/outputs/connected_android_test_additional_output
+```
+
+To inspect the captured trace, open the appropriate `*.trace` file in that
+directory with Android Studio, using `File > Open`.
+
+For more information on the `MethodSampling` and `MethodTracing` profiling
+modes, see the
+[Studio Profiler configuration docs](https://developer.android.com/studio/profile/cpu-profiler#configurations),
+specifically Java Sampled Profiling, and Java Method Tracing.
+
+![Sample flame chart](benchmarking_images/profiling_flame_chart.png "Sample flame chart")
+
+### Advanced: Simpleperf Method Sampling
+
+[Simpleperf](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/)
+offers more accurate profiling for apps than standard method sampling, due to
+lower overhead (as well as C++ profiling support). Simpleperf support will be
+simplified and improved over time.
+
+[Simpleperf app profiling docs](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md).
+
+#### Device
+
+Get an API 28+ device (Or a rooted API 27 device). The rest of this section is
+about *why* those constraints exist, skip if not interested.
+
+Simpleperf has restrictions about where it can be used - Jetpack Benchmark will
+only support API 28+ for now, due to
+[platform/simpleperf constraints](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md#prepare-an-android-application)
+(see last subsection titled "If you want to profile Java code"). Summary is:
+
+-   <=23 (M): Unsupported for Java code.
+
+-   24-25 (N): Requires compiled Java code. We haven't investigated support.
+
+-   26 (O): Requires compiled Java code, and wrapper script. We haven't
+    investigated support.
+
+-   27 (P): Can profile all Java code, but requires `userdebug`/rooted device
+
+-   \>=28 (Q): Can profile all Java code, requires profileable (or
+    `userdebug`/rooted device)
+
+We aren't planning to support profiling debuggable APK builds, since they're
+misleading for profiling.
+
+#### Initial setup
+
+Currently, we rely on Python scripts built by the simpleperf team. We can
+eventually build this into the benchmark library / gradle plugin. Download the
+scripts from AOSP:
+
+```
+# copying to somewhere outside of the androidx repo
+git clone https://android.googlesource.com/platform/system/extras ~/simpleperf
+```
+
+Next configure your path to ensure the ADB that the scripts will use matches the
+androidx tools:
+
+```
+export PATH=$PATH:<path/to/androidx>/prebuilts/fullsdk-<linux or darwin>/platform-tools
+```
+
+Now, setup your device for simpleperf:
+
+```
+~/simpleperf/simpleperf/scripts/api_profiler.py prepare --max-sample-rate 10000000
+```
+
+#### Build and Run, Option 1: Studio (slightly recommended)
+
+Running from Studio is simpler, since you don't have to manually install and run
+the APKs, avoiding Gradle.
+
+Add the following to the benchmark module's build.gradle:
+
+```
+android {
+    defaultConfig {
+        // DO NOT COMMIT!!
+        testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'MethodSamplingSimpleperf'
+        // Optional: Control freq / duration.
+        testInstrumentationRunnerArgument 'androidx.benchmark.profiler.sampleFrequency', '1000000'
+        testInstrumentationRunnerArgument 'androidx.benchmark.profiler.sampleDurationSeconds', '5'
+    }
+}
+```
+
+And run the test or tests you'd like to measure from within Studio.
+
+#### Build and Run, Option 2: Command Line
+
+**Note - this will be significantly simplified in the future**
+
+Since we're not using AGP to pull the files yet, we can't invoke the benchmark
+through Gradle, because Gradle uninstalls after each test run. Instead, let's
+just build and run manually:
+
+```
+./gradlew compose:integration-tests:benchmark:assembleReleaseAndroidTest
+
+adb install -r ../../../out/ui/compose/integration-tests/benchmark/build/outputs/apk/androidTest/release/benchmark-release-androidTest.apk
+
+# run the test (can copy this line from Studio console, when running a benchmark)
+adb shell am instrument -w -m --no-window-animation -e androidx.benchmark.profiling.mode MethodSamplingSimpleperf -e debug false -e class 'androidx.ui.benchmark.test.CheckboxesInRowsBenchmark#toggleCheckbox_draw' androidx.ui.benchmark.test/androidx.benchmark.junit4.AndroidBenchmarkRunner
+```
+
+#### Pull and open the trace
+
+```
+# move the files to host
+# (Note: removes files from device)
+~/simpleperf/simpleperf/scripts/api_profiler.py collect -p androidx.ui.benchmark.test -o ~/simpleperf/results
+
+# create/open the HTML report
+~/simpleperf/simpleperf/scripts/report_html.py -i ~/simpleperf/results/CheckboxesInRowsBenchmark_toggleCheckbox_draw\[1\].data
+```
+
+### Advanced: Studio Profiling
+
+Profiling for allocations and simpleperf profiling requires Studio to capture.
+
+Studio profiling tools require `debuggable=true`. First, temporarily override it
+in your benchmark's `androidTest/AndroidManifest.xml`.
+
+Next choose which profiling you want to do: Allocation, or Sampled (SimplePerf)
+
+`ConnectedAllocation` will help you measure the allocations in a single run of a
+benchmark loop, after warmup.
+
+`ConnectedSampled` will help you capture sampled profiling, but with the more
+detailed / accurate Simpleperf sampling.
+
+Set the profiling type in your benchmark module's `build.gradle`:
+
+```
+android {
+    defaultConfig {
+        // Local only, don't commit this!
+        testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'ConnectedAllocation'
+    }
+}
+```
+
+Run `File > Sync Project with Gradle Files`, or sync if Studio asks you. Now any
+benchmark runs in that project will permit debuggable, and pause before and
+after the test, to allow you to connect a profiler and start recording, and then
+stop recording.
+
+#### Running and Profiling
+
+After the benchmark test starts, you have about 20 seconds to connect the
+profiler:
+
+1.  Click the profiler tab at the bottom
+1.  Click the plus button in the top left, `<device name>`, `<process name>`
+1.  Next step depends on which you intend to capture
+
+#### Allocations
+
+Click the memory section, and right click the window, and select `Record
+allocations`. Approximately 20 seconds later, right click again and select `Stop
+recording`.
diff --git a/docs/benchmarking_images/filter_build.png b/docs/benchmarking_images/filter_build.png
new file mode 100644
index 0000000..f4d15d4
--- /dev/null
+++ b/docs/benchmarking_images/filter_build.png
Binary files differ
diff --git a/docs/benchmarking_images/filter_initial.png b/docs/benchmarking_images/filter_initial.png
new file mode 100644
index 0000000..61bdc30
--- /dev/null
+++ b/docs/benchmarking_images/filter_initial.png
Binary files differ
diff --git a/docs/benchmarking_images/filter_test.png b/docs/benchmarking_images/filter_test.png
new file mode 100644
index 0000000..9a8a0ee
--- /dev/null
+++ b/docs/benchmarking_images/filter_test.png
Binary files differ
diff --git a/docs/benchmarking_images/profiling_flame_chart.png b/docs/benchmarking_images/profiling_flame_chart.png
new file mode 100644
index 0000000..33cd76f
--- /dev/null
+++ b/docs/benchmarking_images/profiling_flame_chart.png
Binary files differ
diff --git a/docs/benchmarking_images/result_plot.png b/docs/benchmarking_images/result_plot.png
new file mode 100644
index 0000000..d0b878e
--- /dev/null
+++ b/docs/benchmarking_images/result_plot.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_bug.png b/docs/benchmarking_images/triage_bug.png
new file mode 100644
index 0000000..816122c
--- /dev/null
+++ b/docs/benchmarking_images/triage_bug.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_cl_list.png b/docs/benchmarking_images/triage_cl_list.png
new file mode 100644
index 0000000..d65a7e2
--- /dev/null
+++ b/docs/benchmarking_images/triage_cl_list.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_complete.png b/docs/benchmarking_images/triage_complete.png
new file mode 100644
index 0000000..3a04763
--- /dev/null
+++ b/docs/benchmarking_images/triage_complete.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_graph.png b/docs/benchmarking_images/triage_graph.png
new file mode 100644
index 0000000..90defbc
--- /dev/null
+++ b/docs/benchmarking_images/triage_graph.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_initial.png b/docs/benchmarking_images/triage_initial.png
new file mode 100644
index 0000000..ea0d033
--- /dev/null
+++ b/docs/benchmarking_images/triage_initial.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_regression.png b/docs/benchmarking_images/triage_regression.png
new file mode 100644
index 0000000..bed4b5f
--- /dev/null
+++ b/docs/benchmarking_images/triage_regression.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_word_cloud.png b/docs/benchmarking_images/triage_word_cloud.png
new file mode 100644
index 0000000..35dbe43
--- /dev/null
+++ b/docs/benchmarking_images/triage_word_cloud.png
Binary files differ
diff --git a/docs/branching.md b/docs/branching.md
new file mode 100644
index 0000000..8b5cd06
--- /dev/null
+++ b/docs/branching.md
@@ -0,0 +1,40 @@
+# AndroidX Branch Workflow
+
+[TOC]
+
+## Single Development Branch [androidx-master-dev]
+
+All feature development occurs in the public AndroidX master dev branch of the
+Android Open Source Project: `androidx-master-dev`. This branch serves as the
+central location and source of truth for all AndroidX library source code. All
+alpha and beta version development, builds, and releases will be done ONLY in
+this branch.
+
+## Release Branches [androidx-\<feature\>-release]
+
+When a library updates to rc (release-candidate) or stable, that library version
+will be snapped over to that library’s release branch. If that release branch
+doesn’t exist, then a release branch will be created for that library, snapped
+from androidx-master-dev at the commit that changed the library to an rc or
+stable version.
+
+Release branches have the following properties:
+
+*   A release branch will contain rc or stable versions of libraries.
+*   Release branches are internal branches.
+*   Release branches can **ONLY** be changed through
+    cherry-picks
+*   Bug-fixes and updates to that rc or stable version will need to be
+    individually cherry-picked
+*   No alpha or beta versions will exist in a release branch.
+*   Toolchain and other library wide changes to androidx-master-dev will be
+    synced to each release branch.
+*   Release branches will have the naming format
+    `androidx-<feature-name>-release`
+*   Release branches will be re-snapped from `androidx-master-dev` for each new
+    minor version release (for example, releasing 2.2.0-rc01 after 2.1.0)
+
+## Platform Developement and AndroidX [androidx-platform-dev]
+
+Platform specific development is done using our INTERNAL platform development
+branch `androidx-platform-dev`.
diff --git a/docs/branching_images/cs_branch_switcher.png b/docs/branching_images/cs_branch_switcher.png
new file mode 100644
index 0000000..660a877
--- /dev/null
+++ b/docs/branching_images/cs_branch_switcher.png
Binary files differ
diff --git a/docs/branching_images/cs_change.png b/docs/branching_images/cs_change.png
new file mode 100644
index 0000000..15c7475
--- /dev/null
+++ b/docs/branching_images/cs_change.png
Binary files differ
diff --git a/docs/branching_images/cs_editor.png b/docs/branching_images/cs_editor.png
new file mode 100644
index 0000000..72ec1ee
--- /dev/null
+++ b/docs/branching_images/cs_editor.png
Binary files differ
diff --git a/docs/branching_images/jetpack_branch_workflow.png b/docs/branching_images/jetpack_branch_workflow.png
new file mode 100644
index 0000000..ca4b094
--- /dev/null
+++ b/docs/branching_images/jetpack_branch_workflow.png
Binary files differ
diff --git a/docs/branching_images/release_branch_diagram.png b/docs/branching_images/release_branch_diagram.png
new file mode 100644
index 0000000..7b54025
--- /dev/null
+++ b/docs/branching_images/release_branch_diagram.png
Binary files differ
diff --git a/docs/do_not_mock.md b/docs/do_not_mock.md
new file mode 100644
index 0000000..d614001
--- /dev/null
+++ b/docs/do_not_mock.md
@@ -0,0 +1,123 @@
+# Do Not Mock, AndroidX
+
+All APIs created in AndroidX **must have a testing story**: how developers
+should write tests for their code that relies on a library, this story should
+not be "use mockito to mock class `Foo`". Your goal as API owner is to **create
+better alternatives** to mocking.
+
+## Why can't I suggest mocks as testing strategy?
+
+Frequently mocks don't follow guarantees outlined in the API they mock. That
+leads to:
+
+*   Significant difference in the behavior that diminishes test value.
+*   Brittle tests, that make hard to evolve both apps and libraries, because new
+    code may start to rely on the guarantees broken in a mock. Let's take a look
+    at a simplified example. So, let's say you mocked a bundle and getString in
+    it:
+
+    ```java
+    Bundle mock = mock(Bundle.class);
+    when(mock.getString("key")).thenReturn("result");
+    ```
+
+    But you don't mock it to simply call `getString()` in your test. A goal is
+    not to test a mock, the goal is always to test your app code, so your app
+    code always interacts with a mock in some way:
+
+    ```java
+    Bundle bundle = mock(Bundle.class);
+    when(mock.getString("key")).thenReturn("result");
+    mycomponent.consume(bundle)
+    ```
+
+    Originally the test worked fine, but over time `component.consume` is
+    evolving, and, for example, it may start to call `containsKey` on the given
+    bundle. But our test passes a mock that don't expect such call and, boom,
+    test is broken. However, component code is completely valid and has nothing
+    to do with the broken test. We observed a lot of issues like that during
+    updates of android SDK and AndroidX libraries to newer versions internally
+    at google. Suggesting to mock our own components is shooting ourselves in
+    the foot, it will make adoption of newer version of libraries even slower.
+
+*   Messy tests. It always starts with simple mock with one method, but then
+    this mock grows with the project, and as a result test code has sub-optimal
+    half-baked class implementation of on top of the mock.
+
+## But it is ok to mock interfaces, right?
+
+It depends. There are interfaces that don't imply any behavior guarantees and
+they are ok to be mocked. However, **not all** interfaces are like that: for
+example, `Map` is an interface but it has a lot of contracts required from
+correct implementation. Examples of interfaces that are ok to mock are callback
+interfaces in general, for example: `View.OnClickListener`, `Runnable`.
+
+## What about spying?
+
+Spying on these classes is banned as well - mockito spies permit stubbing of
+methods just like mocks do, and interaction verification is brittle and
+unnecessary for these classes. Rather than verifying an interaction with a
+class, developers should observe the result of an interaction - the effect of a
+task submitted to an `Executor`, or the presence of a fragment added to your
+layout. If an API in your library misses a way to have such checks, you should
+add methods to do that. If you think it is dangerous to open such methods in the
+main surface of your library, consult with
+[API council](https://sites.google.com/corp/google.com/android-api-council), it
+may have seen similar patterns before. For example, one of the possible ways to
+resolve such issue can be adding test artifact with special capabilities. So
+`fragment-testing` module was created to drive lifecycle of Fragment and ease
+interaction with fragments in tests.
+
+## Avoid mockito in your own tests.
+
+One of the things that would help you to identify if your library is testable
+without mockito is not using mockito yourself. Yes, historically we heavily
+relied on mockito ourselves and old tests are not rewritten, but new tests
+shouldn't follow up that and should take as an example good citizens, for
+example, `-ktx` modules. These modules don't rely on mockito and have concise
+expressive tests.
+
+One of the popular and legit patterns for mockito usage were tests that verify
+that a simple callback-like interface receives correct parameters.
+
+```java
+class MyApi {
+   interface Callback {
+     void onFoo(Value value);
+  }
+  void foo() { … }
+  void registerFooCallback(Callback callback) {...}
+}
+```
+
+In api like the one above, in java 7 tests for value received in `Callback`
+tended to become very wordy without mockito. But now in your tests you can use
+Kotlin and test will be as short as with mockito:
+
+```kotlin
+fun test() {
+    var receivedValue = null
+    myApi.registerCallback { value -> receivedValue = value }
+    myApi.foo()
+   // verify receivedValue
+}
+```
+
+## Don't compromise in API to enable mockito
+
+Mockito on android
+[had an issue](https://github.com/mockito/mockito/issues/1173) with mocking
+final classes. Moreover, internally at google this feature is disabled even for
+non-android code. So you may hear complaints that some of your classes are not
+mockable, however **It is not a reason for open up a class for extension**. What
+you should instead is verify that is possible to write the same test without
+mocking, if not, again you should **provide better alternative in your API**.
+
+## How do I approach testing story for my API?
+
+Best way is to step into developer's shoes and write a sample app that is a
+showcase for your API, then go to the next step - test that code also. If you
+are able to implement tests for your demo app, then users of your API will also
+be able to implement tests for functionalities where your API is also used.
+
+## ~~Use @DoNotMock on most of your APIs ~~(Not available yet)
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..786cc23
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,171 @@
+# FAQ
+
+[TOC]
+
+## General FAQ
+
+### What is AndroidX?
+
+The Android Extension (AndroidX) Libraries provide functionality that extends
+the capabilities of the Android platform. These libraries, which ship separately
+from the Android OS, focus on improving the experience of developing apps
+through broad OS- and device-level compatibility, high-level abstractions to
+simplify and unify platform features, and other new features that target
+developer pain points. To find out more about AndroidX, see the public
+documentation on developer.android.com.
+
+### Why did we move to AndroidX?
+
+Please read our
+[blog post](https://android-developers.googleblog.com/2018/05/hello-world-androidx.html)
+about our migration to AndroidX.
+
+### What happened to the Support Library?
+
+As part of the Jetpack effort to improve developer experience on Android, the
+Support Library team undertook a massive refactoring project. Over the course of
+2017 and 2018, we streamlined and enforced consistency in our packaging,
+developed new policies around vesioning and releasing, and developed tools to
+make it easy for developers to migrate.
+
+### Will there be any more updates to Support Library?
+
+No, Revision 28.0.0 of the Support Library, which launched as stable in
+September 2018, was the last feature release in the android.support package.
+There will be no further releases under Support Library packaging.
+
+### How is AndroidX related to Jetpack?
+
+They are the same thing! In a sentence, AndroidX is the packaging and
+internally-facing development project for all components in Jetpack. Jetpack is
+the external branding for libraries within AndroidX.
+
+In more detail, Jetpack is the external branding for the set of components,
+tools, and guidance that improve the developer experience on Android. AndroidX
+is the open-source development project that defines the workflow, versioning,
+and release policies for ALL libraries included in Jetpack. All libraries within
+the androidx Java package follow a consistent set of API design guidelines,
+conform to SemVer and alpha/beta revision cycles, and use the Android issue
+tracker for bugs and feature requests.
+
+### What AndroidX library versions have been officially released?
+
+You can see all publicly released versions on the interactive
+[Google Maven page](https://dl.google.com/dl/android/maven2/index.html).
+
+### How do I jetify something?
+
+The Standalone Jetifier documentation and download link can be found
+[here](https://developer.android.com/studio/command-line/jetifier), under the
+Android Studio DAC.
+
+### How do I update my library version?
+
+See the steps specified on the version page
+[here](versioning.md#how-to-update-your-version).
+
+### How do I test my change in a separate Android Studio project?
+
+If you're working on a new feature or bug fix in AndroidX, you may want to test
+your changes against another project to verify that the change makes sense in a
+real-world context or that a bug's specific repro case has been fixed.
+
+If you need to be absolutely sure that your test will exactly emulate the
+developer's experience, you can repeatedly build the AndroidX archive and
+rebuild your application. In this case, you will need to create a local build of
+AndroidX's local Maven repository artifact and install it in your Android SDK
+path.
+
+First, use the `createArchive` Gradle task to generate the local Maven
+repository artifact:
+
+```shell
+# Creates <path-to-checkout>/out/dist/sdk-repo-linux-m2repository-##.zip
+./gradlew createArchive
+```
+
+Next, take the ZIP output from this task and extract the contents to the Android
+SDK path that you are using for your alternate (non-AndroidX) version of Android
+Studio. For example, you may be using `~/Android/SDK/extras` if you are using
+the default Android Studio SDK for app development or
+`prebuilts/fullsdk-linux/extras` if you are using fullsdk for platform
+development.
+
+```shell
+# Creates or overwrites android/m2repository
+cd <path-to-sdk>/extras
+unzip <path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip
+```
+
+Finally, in the dependencies section of your standalone project's `build.gradle`
+file, add or update the `compile` entries to reflect the AndroidX modules that
+you would like to test:
+
+```
+dependencies {
+    ...
+    compile "com.android.support:appcompat-v7:26.0.0-SNAPSHOT"
+}
+```
+
+## Version FAQ {#version}
+
+### How are changes in dependency versions propagated?
+
+If you declare `api(project(":depGroupId"))` in your `build.gradle`, then the
+version change will occur automatically. While convienent, be intentional when
+doing so because this causes your library to have a direct dependency on the
+version in development.
+
+If you declare `api("androidx.depGroupId:depArtifactId:1.0.0")`, then the
+version change will need to be done manually and intentionally. This is
+considered best practice.
+
+### How does a library begin work on a new Minor version?
+
+Set the version to the next minor version, as an alpha.
+
+### How does a library ship an API reference documentation bugfix?
+
+Developers obtain API reference documentation from two sources -- HTML docs on
+[d.android.com](https://d.android.com), which are generated from library release
+artifacts, and Javadoc from source JARs on Google Maven.
+
+As a result, documentation bug fixes should be held with other fixes until they
+can go through a normal release cycle. Critical (e.g. P0) documentation issues
+**may** result in a [bugfix](loaf.md#bugfix) release independent of other fixes.
+
+### When does an alpha ship?
+
+For public releases, an alpha ships when the library lead believes it is ready.
+Generally, these occur during the batched bi-weekly (every 2 weeks) release
+because all tip-of-tree dependencies will need to be released too.
+
+### Are there restrictions on when or how often an alpha can ship?
+
+Nope.
+
+### Can Alpha work (ex. for the next Minor release) occur in the primary development branch during Beta API lockdown?
+
+No. This is by design. Focus should be spent on improving the Beta version and
+adding documentation/samples/blog posts for usage!
+
+### Is there an API freeze window between Alpha and Beta while API surface is reviewed and tests are added, but before the Beta is released?
+
+Yes. If any new APIs are added in this window, the beta release will be blocked
+until API review is complete and addressed.
+
+### How often can a Beta release?
+
+As often as needed, however, releases outside of the bi-weekly (every 2 weeks)
+release will need to get approval from the TPM (nickanthony@).
+
+### What are the requirements for moving from Alpha to Beta?
+
+See the [Beta section of Versioning guidelines](versioning.md?#beta) for
+pre-release cycle transition requirements.
+
+### What are the requirements for a Beta launch?
+
+See the [Beta section of Versioning guidelines](versioning.md?#beta) for
+pre-release cycle transition requirements.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..4a29386
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,47 @@
+# What is Jetpack?
+
+## Jetpack Ethos
+
+To create recommended components, tools, and guidance that makes it quick and
+easy to build great Android apps, including pieces both from Google and from
+trusted OSS sources.
+
+## Team Mission
+
+To improve the Android developer experience by providing architectural guidance,
+addressing common pain points, and simplifying the app development process
+through broad compatibility across Android versions and elimination of
+boilerplate code so developers can focus on what makes their app special.
+
+## What is `androidx`?
+
+Artifacts within the `androidx` package comprise the libraries of
+[Android Jetpack](https://developer.android.com/jetpack).
+
+Libraries in the `androidx` package provide functionality that extends the
+capabilities of the Android platform. These libraries, which ship separately
+from the Android OS, focus on improving the experience of developing apps
+through broad OS- and device-level compatibility, high-level abstractions to
+simplify and unify platform features, and other new features that target
+developer pain points.
+
+## What happened to the Support Library?
+
+As part of the Jetpack project to improve developer experience on Android, the
+Support Library team undertook a massive refactoring project. Over the course of
+2017 and 2018, we streamlined and enforced consistency in our packaging,
+developed new policies around versioning and release, and developed tools to
+make it easy for developers to migrate.
+
+Revision 28.0.0 of the Support Library, which launched as stable in September
+2018, was the last feature release in the `android.support` package. There will
+be no further releases under Support Library packaging.
+
+## Quick links
+
+### Filing an issue
+
+Have a bug or feature request? Please check our
+[public Issue Tracker component](http://issuetracker.google.com/issues/new?component=192731&template=842428)
+for duplicates first, then file against the appropriate sub-component according
+to the library package or infrastructure system.
diff --git a/docs/issue_tracking.md b/docs/issue_tracking.md
new file mode 100644
index 0000000..3413ab2
--- /dev/null
+++ b/docs/issue_tracking.md
@@ -0,0 +1,101 @@
+# Issue Lifecycle and Reporting Guidelines
+
+[TOC]
+
+## Issue tracker
+
+The public-facing issue tracker URL is
+[issuetracker.google.com](https://issuetracker.google.com). If you visit this
+URL from a corp account, it will immediately redirect you to the internal-facing
+issue tracker URL. Make sure that any links you paste publicly have the correct
+public-facing URL.
+
+The top-level Jetpack component is
+[`Android Public Tracker > App Development > Jetpack (androidx)`](https://issuetracker.google.com/components/192731/manage#basic).
+
+## Reporting guidelines
+
+Issue Tracker isn't a developer support forum. For support information, consider
+[StackOverflow](http://stackoverflow.com).
+
+Support for Google apps is through
+[Google's support site](http://support.google.com/). Support for third-party
+apps is provided by the app's developer, for example through the contact
+information provided on Google Play.
+
+1.  Search for your bug to see if anyone has already reported it. Don't forget
+    to search for all issues, not just open ones, as your issue might already
+    have been reported and closed. To help you find the most popular results,
+    sort the result by number of stars.
+
+1.  If you find your issue and it's important to you, star it! The number of
+    stars on a bug helps us know which bugs are most important to fix.
+
+1.  If no one has reported your bug, file the bug. First, browse for the correct
+    component -- typically this has a 1:1 correspondence with Maven group ID --
+    and fill out the provided template.
+
+1.  Include as much information in the bug as you can, following the
+    instructions for the bug queue that you're targeting. A bug that simply says
+    something isn't working doesn't help much, and will probably be closed
+    without any action. The amount of detail that you provide, such as a minimal
+    sample project, log files, repro steps, and even a patch set, helps us
+    address your issue.
+
+## Status definitions
+
+| Status   | Description                                                       |
+| -------- | ----------------------------------------------------------------- |
+| New      | The default for public bugs. Waiting for someone to validate,     |
+:          : reproduce, or otherwise confirm that this is actionable.          :
+| Assigned | Pending action from the assignee. May be reassigned.              |
+| Accepted | Actively being worked on by the assignee. Do not reassign.        |
+| Fixed    | Fixed in the development branch. Do not re-open unless the fix is |
+:          : reverted.                                                         :
+| WontFix  | Covers all the reasons we chose to close the issue without taking |
+:          : action (can't repro, working as intended, obsolete).              :
+
+## Priority criteria and SLOs
+
+| Priority | Criteria                       | Resolution time                |
+| -------- | ------------------------------ | ------------------------------ |
+| P0       | This priority is limited to    | Less than 1 day. Don't go home |
+:          : service outages, blocking      : until this is fixed.           :
+:          : issues, or other types of work :                                :
+:          : stoppage such as issues on the :                                :
+:          : Platform chase list requiring  :                                :
+:          : immediate attention.           :                                :
+| P1       | This priority is limited to    | Within the next 7 days         |
+:          : work that requires rapid       :                                :
+:          : resolution, but can be dealt   :                                :
+:          : with in a slightly longer time :                                :
+:          : window than P0.                :                                :
+| P2       | Won't ship without this.       | Within the current release     |
+| P3       | Would rather not ship without  | Less than 365 days             |
+:          : this, but would decide case by :                                :
+:          : case.                          :                                :
+| P4       | Issue has not yet been         | N/A (must triage in under 14   |
+:          : prioritized (default as of Feb : days)                          :
+:          : 2013).                         :                                :
+
+## Issue lifecycle
+
+1.  When an issue is reported, it is set to **Assigned** status for default
+    assignee (typically the [library owner](owners.md)) with a priority of
+    **P4**.
+    *   Some components have an empty default assignee and will be manually
+        assigned by the [triage cop](triage_cop.md)
+1.  Once an issue has been triaged by the assignee, its priority will be raised
+    from **P4** according to severity.
+1.  The issue may still be reassigned at this point.
+    [Bug bounty](onboarding.md#bug-bounty) issues are likely to change
+    assignees.
+1.  A status of **Accepted** means the assignee is actively working on the
+    issue.
+1.  A status of **Fixed** means that the issue has been resolved in the
+    development branch. Please note that it may take some time for the fix to
+    propagate into various release channels (internal repositories, Google
+    Maven, etc.). **Do not** re-open an issue because the fix has not yet
+    propagated into a specific release channel. **Do not** re-open an issue that
+    has been fixed unless the fix was reverted or the exact reported issue is
+    still occurring.
diff --git a/docs/LINT.md b/docs/lint_guide.md
similarity index 86%
rename from docs/LINT.md
rename to docs/lint_guide.md
index 5916b6d..5e40600 100644
--- a/docs/LINT.md
+++ b/docs/lint_guide.md
@@ -1,5 +1,7 @@
 # Adding custom Lint checks
 
+[TOC]
+
 ## Getting started
 
 Lint is a static analysis tool that checks Android project source files. Lint
@@ -47,8 +49,8 @@
 }
 
 dependencies {
-    // compileOnly because we use lintChecks and it doesn't allow other types of deps
-    // this ugly hack exists because of b/63873667
+    // compileOnly because lint runtime is provided when checks are run
+    // Use latest lint for running from IDE to make sure checks always run
     if (rootProject.hasProperty("android.injected.invoked.from.ide")) {
         compileOnly LINT_API_LATEST
     } else {
@@ -81,7 +83,7 @@
 
 Your new module will need to have a registry that contains a list of all of the
 checks to be performed on the library. There is an
-[`IssueRegistry`](https://cs.android.com/android/platform/superproject/+/master:tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java)
+[`IssueRegistry`](https://cs.android.com/android/platform/superproject/+/master:tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java;l=47)
 class provided by the tools team. Extend this class into your own
 `IssueRegistry` class, and provide it with the issues in the module.
 
@@ -103,7 +105,7 @@
 `CURRENT_API` is defined by the Lint API version against which your project is
 compiled, as defined in the module's `build.gradle` file. Jetpack Lint modules
 should compile using Lint API version 3.3 defined in
-[Dependencies.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt;l=84).
+[Dependencies.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt;l=176).
 
 We guarantee that our Lint checks work with versions 3.3-3.6 by running our
 tests with both versions 3.3 and 3.6. For newer versions of Android Studio (and
@@ -268,7 +270,7 @@
 These are Lint checks that will apply to source code files -- primarily Java and
 Kotlin, but can also be used for other similar file types. All code detectors
 that analyze Java or Kotlin files should implement the
-[SourceCodeScanner](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/SourceCodeScanner.kt).
+[SourceCodeScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/SourceCodeScanner.kt).
 
 ### API surface
 
@@ -401,7 +403,7 @@
 These are Lint rules that will apply to resource files including `anim`,
 `layout`, `values`, etc. Lint rules being applied to resource files should
 extend
-[`ResourceXmlDetector`](https://cs.android.com/android/platform/superproject/+/master:tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java).
+[`ResourceXmlDetector`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java).
 The `Detector` must define the issue it is going to detect, most commonly as a
 static variable of the class.
 
@@ -436,7 +438,7 @@
 #### appliesTo
 
 This determines the
-[ResourceFolderType](https://cs.android.com/android/platform/superproject/+/master:tools/base/layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java)
+[ResourceFolderType](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java)
 that the check will run against.
 
 ```kotlin
@@ -503,7 +505,7 @@
 ```
 
 Next, you must test the `Detector` class. The Tools team provides a
-[`LintDetectorTest`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
+[`LintDetectorTest`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
 class that should be extended. Override `getDetector()` to return an instance of
 the `Detector` class:
 
@@ -517,13 +519,13 @@
 getIssues(): MutableList<Issue> = mutableListOf(MyLibraryDetector.ISSUE)
 ```
 
-[`LintDetectorTest`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
+[`LintDetectorTest`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
 provides a `lint()` method that returns a
-[`TestLintTask`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintTask.java).
+[`TestLintTask`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintTask.java).
 `TestLintTask` is a builder class for setting up lint tests. Call the `files()`
 method and provide an `.xml` test file, along with a file stub. After completing
 the set up, call `run()` which returns a
-[`TestLintResult`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintResult.kt).
+[`TestLintResult`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintResult.kt).
 `TestLintResult` provides methods for checking the outcome of the provided
 `TestLintTask`. `ExpectClean()` means the output is expected to be clean because
 the lint rule was followed. `Expect()` takes a string literal of the expected
@@ -536,13 +538,13 @@
 ## Android manifest detector
 
 Lint checks targeting `AndroidManifest.xml` files should implement the
-[XmlScanner](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/XmlScanner.kt)
+[XmlScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/XmlScanner.kt)
 and define target scope in issues as `Scope.MANIFEST`
 
 ## Gradle detector
 
 Lint checks targeting Gradle configuration files should implement the
-[GradleScanner](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/GradleScanner.kt)
+[GradleScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/GradleScanner.kt)
 and define target scope in issues as `Scope.GRADLE_SCOPE`
 
 ### API surface
@@ -599,7 +601,7 @@
 
 Sometimes it is necessary to implement multiple different scanners in a Lint
 detector. For example, the
-[Unused Resource](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java)
+[Unused Resource](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java)
 Lint check implements an XML and SourceCode Scanner in order to determine if
 resources defined in XML files are ever references in the Java/Kotlin source
 code.
@@ -621,16 +623,16 @@
 
 ## Useful classes/packages
 
-### [`SdkConstants`](https://cs.android.com/android/platform/superproject/+/master:tools/base/common/src/main/java/com/android/SdkConstants.java;l=38?q=SdkCon)
+### [`SdkConstants`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:common/src/main/java/com/android/SdkConstants.java)
 
 Contains most of the canonical names for android core library classes, as well
 as XML tag names.
 
 ## Helpful links
 
-[Studio Lint Rules](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks)
+[Studio Lint Rules](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/)
 
-[Lint Detectors and Scanners Source Code](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api)
+[Lint Detectors and Scanners Source Code](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/)
 
 [Creating Custom Link Checks (external)](https://twitter.com/alexjlockwood/status/1176675045281693696)
 
@@ -644,4 +646,4 @@
 
 [ADS 19 Presentation by Alan & Rahul](https://www.youtube.com/watch?v=jCmJWOkjbM0)
 
-[META-INF vs Manifest](https://groups.google.com/forum/#!msg/lint-dev/z3NYazgEIFQ/hbXDMYp5AwAJ)
\ No newline at end of file
+[META-INF vs Manifest](https://groups.google.com/forum/#!msg/lint-dev/z3NYazgEIFQ/hbXDMYp5AwAJ)
diff --git a/docs/manual_prebuilts_dance.md b/docs/manual_prebuilts_dance.md
new file mode 100644
index 0000000..d06e86f
--- /dev/null
+++ b/docs/manual_prebuilts_dance.md
@@ -0,0 +1,22 @@
+# The Manual Prebuilts Dance™
+
+NOTE There is also a [script](releasing.md#the-prebuilts-dance™) that automates
+this step.
+
+Public-facing Jetpack library docs are built from prebuilts to reconcile our
+monolithic docs update process with our independently-versioned library release
+process.
+
+Submit the following changes in the same Gerrit topic so that they merge in the
+same build ID:
+
+1.  Commit your release artifact to the AndroidX AOSP checkout's local Maven
+    repository under `prebuilts/androidx/internal`.
+
+2.  Update the version for your library in the public docs configuration
+    ([docs-public/build.gradle](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:docs-public/build.gradle)).
+    If this is the first time that your library is being published, you will
+    need to add a new entry.
+
+Once both changes are, make sure to note the build ID where they landed. You
+will need to put this in your release request bug for Docs team.
diff --git a/docs/onboarding.md b/docs/onboarding.md
new file mode 100644
index 0000000..5bd331b
--- /dev/null
+++ b/docs/onboarding.md
@@ -0,0 +1,790 @@
+# Getting started
+
+[TOC]
+
+This page describes how to set up your workstation to check out source code,
+make simple changes in Android Studio, and upload commits to Gerrit for review.
+
+This page does **not** cover best practices for the content of changes. Please
+see [Life of a Jetpack Feature](loaf.md) for details on developing and releasing
+a library, [API Guidelines](api_guidelines.md) for best practices regarding
+public APIs, or [Policies and Processes](policies.md) for an overview of the
+constraints placed on changes.
+
+## Workstation setup {#setup}
+
+You will need to install the `repo` tool, which is used for Git branch and
+commit management. If you want to learn more about `repo`, see the
+[Repo Command Reference](https://source.android.com/setup/develop/repo).
+
+### Linux and MacOS {#setup-linux-mac}
+
+First, download `repo` using `curl`.
+
+```shell
+test -d ~/bin || mkdir ~/bin
+curl https://storage.googleapis.com/git-repo-downloads/repo \
+    > ~/bin/repo && chmod 700 ~/bin/repo
+```
+
+Then, modify `~/.bash_profile` (if using `bash`) to ensure you can find local
+binaries from the command line.
+
+```shell
+export PATH=~/bin:$PATH
+```
+
+You will need to either start a new terminal session or run `source
+~/.bash_profile` to pick up the new path.
+
+If you encounter an SSL `CERTIFICATE_VERIFY_FAILED` error or warning about
+Python 2 being no longer supported, you will need to install Python 3 and alias
+your `repo` command to run with `python3`.
+
+```shell {.bad}
+repo: warning: Python 2 is no longer supported; Please upgrade to Python 3.6+.
+```
+
+```shell {.bad}
+Downloading Repo source from https://gerrit.googlesource.com/git-repo
+fatal: Cannot get https://gerrit.googlesource.com/git-repo/clone.bundle
+fatal: error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:777)
+```
+
+First, install Python 3 from the [official website](https://www.python.org).
+Please read the "Important Information" displayed during installation for
+information about SSL/TLS certificate validation and the running the "Install
+Certificates.command".
+
+Next, open your `~/.bash_profile` and add the following lines to wrap the `repo`
+command:
+
+```shell
+# Force repo to run with Python3
+function repo() {
+  command python3 "$(which repo)" $@
+}
+```
+
+### Windows {#setup-win}
+
+Sorry, Windows is not a supported platform for AndroidX development.
+
+## Set up access control {#access}
+
+### Authenticate to AOSP Gerrit {#access-gerrit}
+
+Before you can upload changes, you will need to associate your Google
+credentials with the AOSP Gerrit code review system by signing in to
+[android-review.googlesource.com](https://android-review.googlesource.com) at
+least once using the account you will use to submit patches.
+
+Next, you will need to
+[set up authentication](https://android-review.googlesource.com/new-password).
+This will give you a shell command to update your local Git cookies, which will
+allow you to upload changes.
+
+Finally, you will need to accept the
+[CLA for new contributors](https://android-review.googlesource.com/settings/new-agreement).
+
+## Check out the source {#source}
+
+Like ChromeOS, Chromium, and the Android build system, we develop in the open as
+much as possible. All feature development occurs in the public
+[androidx-master-dev](https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev)
+branch of the Android Open Source Project.
+
+As of 2020/03/20, you will need about 38 GB for a fully-built checkout.
+
+### Synchronize the branch {#source-checkout}
+
+Use the following `repo` commands to check out your branch.
+
+#### Public master development branch {#source-checkout-master}
+
+All development should occur in this branch unless otherwise specified by the
+AndroidX Core team.
+
+The following command will check out the public master development branch:
+
+```shell
+mkdir androidx-master-dev && cd androidx-master-dev
+repo init -u https://android.googlesource.com/platform/manifest \
+    -b androidx-master-dev --partial-clone --clone-filter=blob:limit=10M
+repo sync -c -j8
+```
+
+NOTE On MacOS, if you receive an SSL error like `SSL: CERTIFICATE_VERIFY_FAILED`
+you may need to install Python3 and boot strap the SSL certificates in the
+included version of pip. You can execute `Install Certificates.command` under
+`/Applications/Python 3.6/` to do so.
+
+### Increase Git rename limit {#source-config}
+
+To ensure `git` can detect diffs and renames across significant changes (namely,
+the `androidx.*` package rename), we recommend that you set the following `git
+config` properties:
+
+```shell
+git config --global merge.renameLimit 999999
+git config --global diff.renameLimit 999999
+```
+
+## Explore source code from a browser {#code-search}
+
+`androidx-master-dev` has a publicly-accessible
+[code search](https://cs.android.com/androidx/platform/frameworks/support) that
+allows you to explore all of the source code in the repository. Links to this
+URL may be shared on public Buganizer and other external sites.
+
+We recommend setting up a custom search engine in Chrome as a faster (and
+publicly-accessible) alternative to `cs/`.
+
+### Custom search engine for `androidx-master-dev` {#custom-search-engine}
+
+1.  Open `chrome://settings/searchEngines`
+1.  Click the `Add` button
+1.  Enter a name for your search engine, ex. "AndroidX Code Search"
+1.  Enter a keyword, ex. "csa"
+1.  Enter the following URL:
+    `https://cs.android.com/search?q=%s&ss=androidx%2Fplatform%2Fframeworks%2Fsupport`
+1.  Click the `Add` button
+
+Now you can select the Chrome omnibox, type in `csa` and press tab, then enter a
+query to search for, e.g. `AppCompatButton file:appcompat`, and press the
+`Enter` key to get to the search result page.
+
+## Develop in Android Studio {#studio}
+
+Library development uses a curated version of Android Studio to ensure
+compatibility between various components of the development workflow.
+
+From the `frameworks/support` directory, you can use `ANDROIDX_PROJECTS=MAIN
+./gradlew studio` to automatically download and run the correct version of
+Studio to work on main set of androidx projects. `ANDROIDX_PROJECTS` has several
+other options like `ANDROIDX_PROJECTS=ALL` to open other subsets of the
+projects.
+[settings.gradle](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:settings.gradle)
+file in the repository has these options listed.
+
+```shell
+ANDROIDX_PROJECTS=MAIN ./gradlew studio
+```
+
+Next, open the `framework/support` project root from your checkout. If Studio
+asks you which SDK you would like to use, select `Use project SDK`. Importing
+projects may take a while, but once that finishes you can use Studio as you
+normally would for application or library development -- right-click on a test
+or sample to run or debug it, search through classes, and so on.
+
+If you see any errors (red underlines), click Gradle's elephant button in the
+toolbar ("Sync Project with Gradle Files") and they should resolve once the
+build completes.
+
+> NOTE: You should choose "Use project SDK" when prompted by Studio. If you
+> picked "Android Studio SDK" by mistake, don't panic! You can fix this by
+> opening `File > Project Structure > Platform Settings > SDKs` and manually
+> setting the Android SDK home path to
+> `<project-root>/prebuilts/fullsdk-<platform>`.
+
+> NOTE: If Android Studio's UI looks scaled up, ex. twice the size it should be,
+> you may need to add the following line to your `studio64.vmoptions` file using
+> `Help -> Edit Custom VM Options`:
+>
+> ```
+> -Dsun.java2d.uiScale.enabled=false
+> ```
+
+## Making changes {#changes}
+
+Similar to Android framework development, library developmnent should occur in
+CL-specific working branches. Use `repo` to create, upload, and abandon local
+branches. Use `git` to manage changes within a local branch.
+
+```shell
+cd path/to/checkout/frameworks/support/
+repo start my_branch_name .
+# make necessary code changes
+# use git to commit changes
+repo upload --cbr -t .
+```
+
+The `--cbr` switch automatically picks the current repo branch for upload. The
+`-t` switch sets the Gerrit topic to the branch name, e.g. `my-branch-name`.
+
+## Building {#building}
+
+### Modules and Maven artifacts {#modules-and-maven-artifacts}
+
+To build a specific module, use the module's `assemble` Gradle task. For
+example, if you are working on `core` module use:
+
+```shell
+./gradlew core:core:assemble
+```
+
+Use the `-Pandroidx.allWarningsAsErrors` to make warnings fail your build (same
+as presubmits):
+
+```shell
+./gradlew core:core:assemble -Pandroidx.allWarningsAsErrors
+```
+
+To build every module, run the Lint verifier, verify the public API surface, and
+generate the local Maven repository artifact, use the `createArchive` Gradle
+task:
+
+```shell
+./gradlew createArchive
+```
+
+To run the complete build task that our build servers use, use the
+`buildOnServer` Gradle task:
+
+```shell
+./gradlew buildOnServer
+```
+
+### Attaching a debugger to the build
+
+Gradle tasks, including building a module, may be run or debugged from Android
+Studio's `Gradle` pane by finding the task to be debugged -- for example,
+`androidx > androidx > appcompat > appcompat > build > assemble` --
+right-clicking on it, and then selecting `Debug...`.
+
+Note that debugging will not be available until Gradle sync has completed.
+
+## From the command line
+
+Tasks may also be debugged from the command line, which may be useful if
+`./gradlew studio` cannot run due to a Gradle task configuration issue.
+
+1.  From the configurations dropdown in Studio, select "Edit Configurations".
+1.  Click the plus in the top left to create a new "Remote" configuration. Give
+    it a name and hit "Ok".
+1.  Set breakpoints.
+1.  Run your task with added flags: `./gradlew <your_task_here>
+    -Dorg.gradle.debug=true --no-daemon`
+1.  Hit the "Debug" button to the right of the configuration dropdown to attach
+    to the process.
+
+#### Troubleshooting the debugger
+
+If you get a "Connection refused" error, it's likely because a gradle daemon is
+still running on the port specified in the config, and you can fix this by
+killing the running gradle daemons:
+
+```shell
+./gradlew --stop
+```
+
+Note: This is described in more detail in this
+[Medium article](https://medium.com/grandcentrix/how-to-debug-gradle-plugins-with-intellij-eef2ef681a7b).
+
+#### Attaching to an annotation processor
+
+Annotation processors run as part of the build, to debug them is similar to
+debugging the build.
+
+For a Java project:
+
+```shell
+./gradlew <your_project>:compileDebugJava --no-daemon --rerun-tasks -Dorg.gradle.debug=true
+```
+
+For a Kotlin project:
+
+```shell
+./gradlew <your_project>:compileDebugKotlin --no-daemon --rerun-tasks -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy="in-process" -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket\,address=5005\,server=y\,suspend=n"
+```
+
+### Optional: Enabling internal menu in IntelliJ/Studio
+
+To enable tools such as `PSI tree` inside of IntelliJ/Studio to help debug
+Android Lint checks and Metalava, you can enable the
+[internal menu](https://www.jetbrains.org/intellij/sdk/docs/reference_guide/internal_actions/enabling_internal.html)
+which is typically used for plugin and IDE development.
+
+### Reference documentation {#docs}
+
+Our reference docs (Javadocs and KotlinDocs) are published to
+https://developer.android.com/reference/androidx/packages and may be built
+locally.
+
+NOTE `./gradlew tasks` always has the canonical task information! When in doubt,
+run `./gradlew tasks`
+
+#### Javadocs
+
+To build API reference docs for tip-of-tree Java source code, run the Gradle
+task:
+
+```
+./gradlew disttipOfTreeDocs
+```
+
+This will output docs in the zip file:
+`{androidx-master-dev}/out/dist/android-support-tipOfTree-docs-0.zip`, as well
+as in local html files that you can check from your browser:
+`{androidx-master-dev}/out/androidx/build/javadoc/tipOfTree/offline/reference/packages.html`
+
+#### KotlinDocs
+
+To build API reference docs for tip-of-tree Kotlin source code, run the Gradle
+task:
+
+```
+./gradlew distTipOfTreeDokkaDocs
+```
+
+This will output docs in the zip file:
+`{androidx-master-dev}/out/dist/dokkaTipOfTreeDocs-0.zip`
+
+#### Release docs
+
+To build API reference docs for published artifacts formatted for use on
+[d.android.com](http://d.android.com), run the Gradle command:
+
+```
+./gradlew distpublicDocs
+```
+
+This will create the artifact
+`{androidx-master-dev}/out/dist/android-support-public-docs-0.zip`. This command
+builds docs based on the version specified in
+`{androidx-master-dev-checkout}/frameworks/support/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt`
+and uses the prebuilt checked into
+`{androidx-master-dev-checkout}/prebuilts/androidx/internal/androidx/`. We
+colloquially refer to this two step process of (1) updating PublishDocsRules.kt
+and (2) checking in a prebuilt artifact into the prebuilts directory as
+[The Prebuilts Dance](releasing.md#the-prebuilts-dance™). So, to build javadocs
+that will be published to
+https://developer.android.com/reference/androidx/packages, both of these steps
+need to be completed.
+
+Once you done the above steps, Kotlin docs will also be generated, with the only
+difference being that we use the Gradle command:
+
+```
+./gradlew distPublicDokkaDocs
+```
+
+which generates the kotlin docs artifact
+`{androidx-master-dev}/out/dist/dokkaPublicDocs-0.zip`
+
+### Updating public APIs {#updating-public-apis}
+
+Public API tasks -- including tracking, linting, and verifying compatibility --
+are run under the following conditions based on the `androidx` configuration
+block, evaluated in order:
+
+*   `runApiTasks=Yes` => yes
+*   `runApiTasks=No` => no
+*   `toolingProject=true` => no
+*   `mavenVersion` or group version not set => no
+*   Has an existing `api/` directory => yes
+*   `publish=SNAPSHOT_AND_RELEASE` => yes
+*   Otherwise, no
+
+If you make changes to tracked public APIs, you will need to acknowledge the
+changes by updating the `<component>/api/current.txt` and associated API files.
+This is handled automatically by the `updateApi` Gradle task:
+
+```shell
+# Run updateApi for all modules.
+./gradlew updateApi
+
+# Run updateApi for a single module, ex. appcompat-resources in group appcompat.
+./gradlew :appcompat:appcompat-resources:updateApi
+```
+
+If you change the public APIs without updating the API file, your module will
+still build **but** your CL will fail Treehugger presubmit checks.
+
+### Release notes & the `Relnote:` tag {#relnote}
+
+Prior to releasing, release notes are pre-populated using a script and placed
+into a Google Doc. The Google Doc is manually double checked by library owners
+before the release goes live. To auto-populate your release notes, you can use
+the semi-optional commit tag `Relnote:` in your commit, which will automatically
+include that message the commit in the pre-populated release notes.
+
+The presence of a `Relnote:` tag is required for API changes in
+`androidx-master-dev`.
+
+#### How to use it?
+
+One-line release note:
+
+``` {.good}
+Relnote: Fixed a critical bug
+```
+
+``` {.good}
+Relnote: "Fixed a critical bug"
+```
+
+``` {.good}
+Relnote: Added the following string function: `myFoo(\"bar\")`
+```
+
+Multi-line release note:
+
+Note: If the following lines do not contain an indent, you may hit b/165570183.
+
+``` {.good}
+Relnote: "We're launching this awesome new feature!  It solves a whole list of
+    problems that require a lot of explaining! "
+```
+
+``` {.good}
+Relnote: """Added the following string function: `myFoo("bar")`
+    It will fix cases where you have to call `myFoo("baz").myBar("bar")`
+    """
+```
+
+Opt out of the Relnote tag:
+
+``` {.good}
+Relnote: N/A
+```
+
+``` {.good}
+Relnote: NA
+```
+
+NOT VALID:
+
+``` {.bad}
+Relnote: This is an INVALID multi-line release note.  Our current scripts won't
+include anything beyond the first line.  The script has no way of knowing when
+the release note actually stops.
+```
+
+``` {.bad}
+Relnote: This is an INVALID multi-line release note.  "Quotes" need to be
+  escaped in order for them to be parsed properly.
+```
+
+### Common build errors
+
+#### Diagnosing build failures
+
+If you've encountered a build failure and you're not sure what is triggering it,
+then please run
+`./development/diagnose-build-failure/diagnose-build-failure.sh`.
+
+This script can categorize your build failure into one of the following
+categories:
+
+*   The Gradle Daemon is saving state in memory and triggering a failure
+*   Your source files have been changed and/or incompatible git commits have
+    been checked out
+*   Some file in the out/ dir is triggering an error
+    *   If this happens, diagnose-build-failure.sh should also identify which
+        file(s) specifically
+*   The build is nondeterministic and/or affected by timestamps
+*   The build via gradlew actually passes and this build failure is specific to
+    Android Studio
+
+Some more-specific build failures are listed below in this page.
+
+#### Out-of-date platform prebuilts
+
+Like a normal Android library developed in Android Studio, libraries within
+`androidx` are built against prebuilts of the platform SDK. These are checked in
+to the `prebuilts/fullsdk-darwin/platforms/<android-version>` directory.
+
+If you are developing against pre-release platform APIs in the internal
+`androidx-platform-dev` branch, you may need to update these prebuilts to obtain
+the latest API changes.
+
+### Missing external dependency
+
+If Gradle cannot resolve a dependency listed in your `build.gradle`, you may
+need to import the corresponding artifact into `prebuilts/androidx/external`.
+Our workflow does not automatically download artifacts from the internet to
+facilitate reproducible builds even if remote artifacts are changed.
+
+You can download a dependency by running:
+
+```shell
+cd frameworks/support && ./development/importMaven/import_maven_artifacts.py -n 'someGroupId:someArtifactId:someVersion'
+```
+
+This will create a change within the `prebuilts/androidx/external` directory.
+Make sure to upload this change before or concurrently (ex. in the same Gerrit
+topic) with the dependent library code.
+
+Libraries typically reference dependencies using constants defined in
+[`Dependencies.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt),
+so please update this file to include a constant for the version of the library
+that you have checked in. You will reference this constant in your library's
+`build.gradle` dependencies.
+
+#### Updating an existing dependency
+
+If an older version of a dependency prebuilt was already checked in, please
+manually remove it within the same CL that adds the new prebuilt. You will also
+need to update `Dependencies.kt` to reflect the version change.
+
+#### My gradle build fails with "Cannot invoke method getURLs() on null object"
+
+You're using Java 9's javac, possibly because you ran envsetup.sh from the
+platform build or specified Java 9 as the global default Java compiler. For the
+former, you can simply open a new shell and avoid running envsetup.sh. For the
+latter, we recommend you set Java 8 as the default compiler using sudo
+update-java-alternatives; however, if you must use Java 9 as the default then
+you may alternatively set JAVA_HOME to the location of the Java 8 SDK.
+
+#### My gradle build fails with "error: cannot find symbol" after making framework-dependent changes.
+
+You probably need to update the prebuilt SDK used by the gradle build. If you
+are referencing new framework APIs, you will need to wait for the framework
+changes to land in an SDK build (or build it yourself) and then land in both
+prebuilts/fullsdk and prebuilts/sdk. See
+[Updating SDK prebuilts](playbook.md#prebuilts-fullsdk) for more information.
+
+#### How do I handle refactoring a framework API referenced from a library?
+
+Because AndroidX must compile against both the current framework and the latest
+SDK prebuilt, and because compiling the SDK prebuilt depends on AndroidX, you
+will need to refactor in stages: Remove references to the target APIs from
+AndroidX Perform the refactoring in the framework Update the framework prebuilt
+SDK to incorporate changes in (2) Add references to the refactored APIs in
+AndroidX Update AndroidX prebuilts to incorporate changes in (4)
+
+## Testing {#testing}
+
+AndroidX libraries are expected to include unit or integration test coverage for
+100% of their public API surface. Additionally, all CLs must include a `Test:`
+stanza indicating which tests were used to verify correctness. Any CLs
+implementing bug fixes are expected to include new regression tests specific to
+the issue being fixed
+
+See the [Testing](testing.md) page for more resources on writing, running, and
+monitoring tests.
+
+### AVD Manager
+
+The Android Studio instance started by `./gradlew studio` uses a custom SDK
+directory, which means any virtual devices created by a "standard" non-AndroidX
+instance of Android Studio will be _visible_ from the `./gradlew studio`
+instance but will be unable to locate the SDK artifacts -- they will display a
+`Download` button.
+
+You can either use the `Download` button to download an extra copy of the SDK
+artifacts _or_ you can set up a symlink to your "standard" non-AndroidX SDK
+directory to expose your existing artifacts to the `./gradlew studio` instance:
+
+```shell
+# Using the default MacOS Android SDK directory...
+ln -s /Users/$(whoami)/Library/Android/sdk/system-images \
+      ../../prebuilts/fullsdk-darwin/system-images
+```
+
+### Benchmarking {#testing-benchmarking}
+
+Libraries are encouraged to write and monitor performance benchmarks. See the
+[Benchmarking](benchmarking.md) page for more details.
+
+## Library snapshots {#snapshots}
+
+### Quick how to
+
+Add the following snippet to your build.gradle file, replacing `buildId` with a
+snapshot build Id.
+
+```groovy {highlight=context:[buildId]}
+allprojects {
+    repositories {
+        google()
+        jcenter()
+        maven { url 'https://androidx.dev/snapshots/builds/[buildId]/artifacts/repository' }
+    }
+}
+```
+
+You must define dependencies on artifacts using the SNAPSHOT version suffix, for
+example:
+
+```groovy {highlight=context:SNAPSHOT}
+dependencies {
+    implementation "androidx.core:core:1.2.0-SNAPSHOT"
+}
+```
+
+### Where to find snapshots
+
+If you want to use unreleased `SNAPSHOT` versions of `androidx` artifacts, you
+can find them on either our public-facing build server:
+
+`https://ci.android.com/builds/submitted/<build_id>/androidx_snapshot/latest`
+
+or on our slightly-more-convenient [androidx.dev](https://androidx.dev) site:
+
+`https://androidx.dev/snapshots/builds/<build-id>/artifacts/repository` for a
+specific build ID
+
+`https://androidx.dev/snapshots/builds/latest/artifacts/repository` for
+tip-of-tree snapshots
+
+### Obtaining a build ID
+
+To browse build IDs, you can visit either
+[androidx-master-dev](https://ci.android.com/builds/branches/aosp-androidx-master-dev/grid?)
+on ci.android.com or [Snapshots](https://androidx.dev/snapshots/builds) on the
+androidx.dev site.
+
+Note that if you are using androidx.dev, you may substitute `latest` for a build
+ID to use the last known good build.
+
+To manually find the last known good `build-id`, you have several options.
+
+#### Snapshots on androidx.dev
+
+[Snapshots](https://androidx.dev/snapshots/builds) on androidx.dev only lists
+usable builds.
+
+#### Programmatically via `jq`
+
+Install `jq`:
+
+```shell
+sudo apt-get install jq
+```
+
+```shell
+ID=`curl -s "https://ci.android.com/builds/branches/aosp-androidx-master-dev/status.json" | jq ".targets[] | select(.ID==\"aosp-androidx-master-dev.androidx_snapshot\") | .last_known_good_build"` \
+  && echo https://ci.android.com/builds/submitted/"${ID:1:-1}"/androidx_snapshot/latest/raw/repository/
+```
+
+#### Android build server
+
+Go to
+[androidx-master-dev](https://ci.android.com/builds/branches/aosp-androidx-master-dev/grid?)
+on ci.android.com.
+
+For `androidx-snapshot` target, wait for the green "last known good build"
+button to load and then click it to follow it to the build artifact URL.
+
+### Using in a Gradle build
+
+To make these artifacts visible to Gradle, you need to add it as a respository:
+
+```groovy
+allprojects {
+    repositories {
+        google()
+        maven {
+          // For all Jetpack libraries (including Compose)
+          url 'https://androidx.dev/snapshots/builds/<build-id>/artifacts/repository'
+        }
+    }
+}
+```
+
+Note that the above requires you to know the `build-id` of the snapshots you
+want.
+
+#### Specifying dependencies
+
+All artifacts in the snapshot repository are versioned as `x.y.z-SNAPSHOT`. So
+to use a snapshot artifact, the version in your `build.gradle` will need to be
+updated to `androidx.<groupId>:<artifactId>:X.Y.Z-SNAPSHOT`
+
+For example, to use the `core:core:1.2.0-SHAPSHOT` snapshot, you would add the
+following to your `build.gradle`:
+
+```
+dependencies {
+    ...
+    implementation("androidx.core:core:1.2.0-SNAPSHOT")
+    ...
+}
+```
+
+## FAQ {#faq}
+
+### How do I test my change in a separate Android Studio project? {#faq-test-change-studio}
+
+If you're working on a new feature or bug fix in AndroidX, you may want to test
+your changes against another project to verify that the change makes sense in a
+real-world context or that a bug's specific repro case has been fixed.
+
+If you need to be absolutely sure that your test will exactly emulate the
+developer's experience, you can repeatedly build the AndroidX archive and
+rebuild your application. In this case, you will need to create a local build of
+AndroidX's local Maven repository artifact and install it in your Android SDK
+path.
+
+First, use the `createArchive` Gradle task to generate the local Maven
+repository artifact:
+
+```shell
+# Creates <path-to-checkout>/out/dist/sdk-repo-linux-m2repository-##.zip
+./gradlew createArchive
+```
+
+Next, take the ZIP output from this task and extract the contents to the Android
+SDK path that you are using for your alternate (non-AndroidX) version of Android
+Studio. For example, you may be using `~/Android/SDK/extras` if you are using
+the default Android Studio SDK for app development or
+`prebuilts/fullsdk-linux/extras` if you are using fullsdk for platform
+development.
+
+```shell
+# Creates or overwrites android/m2repository
+cd <path-to-sdk>/extras
+unzip <path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip
+```
+
+In the project's 'build.gradle' within 'repositories' notify studio of the
+location of m2repository:
+
+```groovy
+allprojects {
+    repositories {
+        ...
+        maven {
+            url "<path-to-sdk>/extras/m2repository"
+        }
+    }
+}
+```
+
+NOTE Gradle resolves dependencies in the order that the repositories are defined
+(if 2 repositories can resolve the same dependency, the first listed will do so
+and the second will not). Therefore, if the library you are testing has the same
+group, artifact, and version as one already published, you will want to list
+your custom maven repo first.
+
+Finally, in the dependencies section of your standalone project's `build.gradle`
+file, add or update the `implementation` entries to reflect the AndroidX modules
+that you would like to test. Example:
+
+```
+dependencies {
+    ...
+    implementation "androidx.appcompat:appcompat::1.0.0-alpha02"
+}
+```
+
+If you are testing your changes in the Android Platform code, you can replace
+the module you are testing
+`YOUR_ANDROID_PATH/prebuilts/sdk/current/androidx/m2repository` with your own
+module. We recommend only replacing the module you are modifying instead of the
+full m2repository to avoid version issues of other modules. You can either take
+the unzipped directory from
+`<path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip`, or from
+`<path-to-checkout>/out/androidx/build/support_repo/` after buiding `androidx`.
+Here is an example of replacing the RecyclerView module:
+
+```shell
+$TARGET=YOUR_ANDROID_PATH/prebuilts/sdk/current/androidx/m2repository/androidx/recyclerview/recyclerview/1.1.0-alpha07;
+rm -rf $TARGET;
+cp -a <path-to-sdk>/extras/m2repository/androidx/recyclerview/recyclerview/1.1.0-alpha07 $TARGET
+```
+
+Make sure the library versions are the same before and after replacement. Then
+you can build the Android platform code with the new `androidx` code.
diff --git a/docs/onboarding_images/image1.png b/docs/onboarding_images/image1.png
new file mode 100644
index 0000000..9a32d42
--- /dev/null
+++ b/docs/onboarding_images/image1.png
Binary files differ
diff --git a/docs/onboarding_images/image2.png b/docs/onboarding_images/image2.png
new file mode 100644
index 0000000..9c215f1
--- /dev/null
+++ b/docs/onboarding_images/image2.png
Binary files differ
diff --git a/docs/onboarding_images/image3.png b/docs/onboarding_images/image3.png
new file mode 100644
index 0000000..e672255
--- /dev/null
+++ b/docs/onboarding_images/image3.png
Binary files differ
diff --git a/docs/policies.md b/docs/policies.md
new file mode 100644
index 0000000..b099299
--- /dev/null
+++ b/docs/policies.md
@@ -0,0 +1,190 @@
+## AndroidX policies and processes
+
+This document is intended to describe release policies that affect the workflow
+of an engineer developing within the AndroidX libraries. It also describes the
+process followed by a release engineer or TPM to take a development branch and
+publish it as a release on Google Maven.
+
+Policies and processes automated via tooling are noted in
+<span style="color:#bf9000;">yellow</span>.
+
+[TOC]
+
+## Project directory structure {#directory-structure}
+
+Libraries developed in AndroidX follow a consistent project naming and directory
+structure.
+
+Library groups should organize their modules into directories and module names
+(in brackets) as:
+
+```
+<feature-name>/
+  <feature-name>-<sub-feature>/ [<feature-name>:<feature-name>-<sub-feature>]
+  integration-tests/
+    testapp/ [<feature-name>:testapp]
+    testlib/ [<feature-name>:testlib]
+    samples/ [<feature-name>:samples]
+```
+
+For example, the `room` library group's directory structure is:
+
+```
+room/
+  common/ [room:room-common]
+  ...
+  rxjava2/ [room:room-rxjava2]
+  testing/ [room:room-testing]
+  integration-tests/
+    testapp/ [room:testapp]
+    testapp-kotlin/ [room:testapp-kotlin]
+```
+
+## Terminology {#terminology}
+
+**Artifact**
+:   Previously referred to as "a Support Library library." A library --
+    typically Java or Android -- that maps to a single Maven artifact, ex.
+    `androidx.recyclerview:recyclerview`. An artifact is associated with a
+    single Android Studio module and a directory containing a `build.gradle`
+    configuration, resources, and source code.
+
+**API Council**
+:   A committee that reviews Android APIs, both platform and library, to ensure
+    they are consistent and follow the best-practices defined in our API
+    guidelines.
+
+**Semantic Versioning (SemVer)**
+:   A versioning standard developed by one of the co-founders of GitHub that is
+    understood by common dependency management systems, including Maven. In this
+    document, we are referring specifically to
+    [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
+
+## Managing versions {#managing-versions}
+
+This section outlines the steps for a variety of common versioning-related
+tasks. Artifact versions should **only** be modified by their owners as
+specified in the artifact directory's `OWNERS` file.
+
+Artifact versions are specified in
+[`LibraryVersions.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt).
+Versions are bound to your artifact in the `supportLibrary` block in your
+artifact's `build.gradle` file. The `Version` class validates the version string
+at build time.
+
+In the
+[`LibraryVersions.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt)
+file:
+
+```
+object LibraryVersions {
+    val SNAZZY_ARTIFACT = Version("1.1.0-alpha03")
+}
+```
+
+In your artifact's `build.gradle` file:
+
+```
+import androidx.build.LibraryVersions
+
+supportLibrary {
+   mavenVersion = LibraryVersions.SNAZZY_ARTIFACT
+}
+```
+
+#### Finalizing for release {#finalizing-for-release}
+
+When the artifact is ready for release, its versioned API file should be
+finalized to ensure that the subsequent versioned release conforms to the
+versioning policies.
+
+```
+./gradlew <module>:finalizeApi
+```
+
+This will prevent any subsequent changes to the API surface until the artifact
+version is updated. To update the artifact version and allow changes within the
+semantic versioning contract, simply update the version string in the artifact's
+`build.gradle` (see [Workflow](#workflow) introduction).
+
+To avoid breaking the development workflow, we recommend that API finalization
+and version string updates be submitted as a single CL.
+
+## Dependencies {#dependencies}
+
+Artifacts may depend on other artifacts within AndroidX as well as sanctioned
+third-party libraries.
+
+### Versioned artifacts {#versioned-artifacts}
+
+One of the most difficult aspects of independently-versioned releases is
+maintaining compatibility with public artifacts. In a mono repo such as Google's
+repository or Android Git at master revision, it's easy for an artifact to
+accidentally gain a dependency on a feature that may not be released on the same
+schedule.
+
+#### Pre-release dependencies {#pre-release-dependencies}
+
+Pre-release suffixes **must** propagate up the dependency tree. For example, if
+your artifact has API-type dependencies on pre-release artifacts, ex.
+`1.1.0-alpha01`, then your artifact must also carry the `alpha` suffix. If you
+only have implementation-type dependencies, your artifact may carry either the
+`alpha` or `beta` suffix.
+
+#### Pinned versions {#pinned-versions}
+
+To avoid issues with dependency versioning, consider pinning your artifact's
+dependencies to the oldest version (available via local `maven_repo` or Google
+Maven) that satisfies the artifact's API requirements. This will ensure that the
+artifact's release schedule is not accidentally tied to that of another artifact
+and will allow developers to use older libraries if desired.
+
+```
+dependencies {
+   api("androidx.collection:collection:1.0.0")
+   ...
+}
+```
+
+Artifacts should be built and tested against both pinned and tip-of-tree
+versions of their dependencies to ensure behavioral compatibility.
+
+#### Non-Pinned versions {#nonpinned-versions}
+
+Below is an example of a non-pinned dependency. It ties the artifact's release
+schedule to that of the dependency artifact, because the dependency will need to
+be released at the same time.
+
+```
+dependencies {
+   api(project(":collection"))
+   ...
+}
+```
+
+### Non-public APIs {#non-public-apis}
+
+Artifacts may depend on non-public (e.g. `@hide`) APIs exposed within their own
+artifact or another artifact in the same `groupId`; however, cross-artifact
+usages are subject to binary compatibility guarantees and
+`@RestrictTo(Scope.LIBRARY_GROUP)` APIs must be tracked like public APIs.
+
+```
+Dependency versioning policies are enforced at build time in the createArchive task. This task will ensure that pre-release version suffixes are propagated appropriately.
+
+Cross-artifact API usage policies are enforced by the checkApi and checkApiRelease tasks (see Life of a release).
+```
+
+### Third-party libraries {#third-party-libraries}
+
+Artifacts may depend on libraries developed outside of AndroidX; however, they
+must conform to the following guidelines:
+
+*   Prebuilt **must** be checked into Android Git with both Maven and Make
+    artifacts
+    *   `prebuilts/maven_repo` is recommended if this dependency is only
+        intended for use with AndroidX artifacts, otherwise please use
+        `external`
+*   Prebuilt directory **must** contains an OWNERS file identifying one or more
+    individual owners (e.g. NOT a group alias)
+*   Library **must** be approved by legal
diff --git a/docs/principles.md b/docs/principles.md
new file mode 100644
index 0000000..6dba721
--- /dev/null
+++ b/docs/principles.md
@@ -0,0 +1,135 @@
+# Jetpack Principles
+
+[TOC]
+
+## Ethos of Jetpack
+
+To create components, tools, and guidance that makes it quick and easy to build
+great Android apps, including contributions from Google and the open-source
+community.
+
+## Core Principles of a Jetpack Library
+
+Jetpack libraries provide the following guarantees to Android Developers:
+
+_formatted as “Jetpack libraries are…” with sub-points “Libraries should…”_
+
+### 1. Optimized for external client adoption
+
+-   Libraries should work for first-party clients and may even have optional
+    modules tailored specifically to first-party needs, but primary
+    functionality should target external developers.
+-   Measure success by 3p client adoption, followed by 1p client adoption.
+
+### 2. Designed to satisfy real-world use cases
+
+-   Meet developers where they are and solve the problems that they have
+    building apps -- not designed to just provide parity with existing platform
+    APIs and features
+-   Expose modules that are tightly-scoped to **developer pain points**
+    -   Smaller building blocks for external developers by scoping disjoint use
+        cases that are likely not to co-exist in a single app to individual
+        modules.
+-   Implement layered complexity, with **simple top-level APIs**
+    -   Complicated use case support must not be at the expense of increasing
+        API complexity for the most common simpler use cases.
+-   Have **backing data or a researched hypothesis** (research, demand etc) to
+    prove the library is necessary and sufficient.
+
+### 3. Aware of the existing developer ecosystem
+
+-   Avoid reinventing the wheel -- do not create a new library where one already
+    exists that is accepted by the community as a best practice
+
+### 4. Consistent with the rest of Jetpack
+
+-   Ensure that concepts learned in one component can be seen and understood in
+    other components
+-   Leverage Jetpack and community standards, for example:
+    -   For async work, uses Kotlin coroutines and/or Kotlin flow
+    -   For data persistence, uses Jetpack DataStore for simple and small data
+        and uses Room for more complicated Data
+
+### 5. Developed as open-source and compatible with AOSP Android
+
+-   Expose a unified developer-facing API surface across the Android ecosystem
+-   Avoid proprietary services or closed-source libraries for core
+    functionality, and instead provide integration points that allow a developer
+    to choose a proprietary service as the backing implementation
+-   Develop in AOSP to provide visibility into new features and bug fixes and
+    encourage external participation
+
+### 6. Written using language-idiomatic APIs
+
+-   Write APIs that feel natural for clients using both
+    [Kotlin](https://developer.android.com/kotlin/interop) and Java
+
+### 7. Compatible with a broad range of API levels
+
+-   Support older platforms and API levels according to client needs
+-   Provide continued maintenance to ensure compatibility with newer platforms
+-   Design with the expectation that every Jetpack API is **write-once,
+    run-everywhere** for Android with graceful degradation where necessary
+
+### 8. Integrated with best practices
+
+-   Guide developers toward using existing Jetpack best-practice libraries,
+    including Architecture Components
+
+### 9. Designed for tooling and testability
+
+-   Write adequate unit and integration tests for the library itself
+-   Provide developers with an accompanying testing story for integration
+    testing their own applications (ex. -testing artifacts that some libraries
+    expose)
+    -   Robolectric shouldn’t need to shadow the library classes
+    -   Ex. Room has in-memory testing support
+-   Build tooling concurrent with the library when possible, and with tooling in
+    mind otherwise
+
+### 10. Released using a clearly-defined process
+
+-   Follow Semantic Versioning and pre-release revision guidelines where each
+    library moves through alpha, beta, and rc revisions to gain feedback and
+    ensure stability
+
+### 11. Well-documented
+
+-   Provide developers with getting started and use case documentation on
+    d.android.com in addition to clear API reference documentation
+
+### 12. Supported for long-term use
+
+-   Plan for long-term support and maintenance
+
+### 13. Examples of modern development
+
+-   Where possible, targeting the latest languages, OS features, and tools. All
+    new libraries should be written in Kotlin first. Existing libraries
+    implemented in Java should add Kotlin extension libraries to improve the
+    interoperability of the Java APIs from Kotlin. New libraries written in Java
+    require a significant business reason on why a dependency in Kotlin cannot
+    be taken. The following is the order of preference, with each lower tier
+    requiring a business reason:
+    1.  Implemented in Kotlin that compiles to Java 8 bytecode
+    2.  Implemented in Java 8, with `-ktx` extensions for Kotlin
+        interoperability
+    3.  Implemented in Java 7, with `-ktx` extensions for Kotlin
+        interoperability
+
+### 14. High quality APIs and ownership
+
+-   All Jetpack libraries are expected to abide by Android and Jetpack API
+    Guidelines
+
+## Target Audience
+
+Jetpack libraries are used by a wide variety of external developers, from
+individuals working on their first Android app to huge corporations developing
+large-scale production apps. Generally, however, Jetpack libraries are designed
+to focus on small- to medium-sized app development teams.
+
+-   Note: If the library targets a niche set of apps, the developer pain
+    point(s) addressed by the Jetpack library must be significant enough to
+    justify its need.
+    -   Example : Jetpack Enterprise library
diff --git a/docs/testing.md b/docs/testing.md
new file mode 100644
index 0000000..b937bb9
--- /dev/null
+++ b/docs/testing.md
@@ -0,0 +1,246 @@
+# Testing
+
+[TOC]
+
+AndroidX contains unit and integration tests that are run automatically when a
+change is uploaded. It also contains a number of sample applications that are
+useful for demonstrating how to use features as well as performing manual
+testing.
+
+## Adding tests {#adding}
+
+For an example of how to set up simple unit and integration tests in a new
+module, see
+[aosp/1189799](https://android-review.googlesource.com/c/platform/frameworks/support/+/1189799).
+For an example of how to set up Espresso-powered integration tests, see the
+`preference` library's
+[`build.gradle`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:preference/preference/build.gradle)
+and
+[`EditTextPreferenceTest.java`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:preference/preference/src/androidTest/java/androidx/preference/tests/EditTextPreferenceTest.java)
+files.
+
+The currently allowed test runners for on-device tests are
+[`AndroidJUnitRunner`](https://developer.android.com/training/testing/junit-runner)
+and
+[`Parameterized`](https://junit.org/junit4/javadoc/4.12/org/junit/runners/Parameterized.html).
+
+### What gets tested, and when
+
+We use the
+[AffectedModuleDetector](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt)
+to determine what projects have changed since the last merge.
+
+In presubmit, "affected" modules will run all host and device tests regardless
+of size. Modules that _depend_ on affected modules will run all host tests, but
+will only run device tests annotated with `@SmallTest` or `@MediumTest`.
+
+When changes are made that can't be associated with a module, are in the root of
+the checkout, or are within `buildSrc`, then all host tests and all device tests
+annotated with `@SmallTest` or `@MediumTest` will be run for all modules.
+
+Presubmit tests represent only a subset of the devices on which our tests run.
+The remaining devices are tested only in postsubmit. In postsubmit, all host and
+device tests are run for all modules.
+
+### Test annotations
+
+#### Test size
+
+All device tests *should* be given a size annotation, which is one of:
+
+*   [`@SmallTest`](https://developer.android.com/reference/androidx/test/filters/SmallTest)
+*   [`@MediumTest`](https://developer.android.com/reference/androidx/test/filters/MediumTest)
+*   [`@LargeTest`](https://developer.android.com/reference/androidx/test/filters/LargeTest)
+
+If a device test is _not_ annotated with its size, it will be considered a
+`@LargeTest` by default. Host tests do not need to be annotated with their size,
+as all host tests are run regardless of size.
+
+This annotation can occur at either the class level or individual test level.
+After API level 27, timeouts are enforced based on this annotation.
+
+Annotation    | Max duration | Timeout after
+------------- | ------------ | -------------
+`@SmallTest`  | 200ms        | 300ms
+`@MediumTest` | 1000ms       | 1500ms
+`@LargeTest`  | 100000ms     | 120000ms
+
+Small tests should be less than 200ms, and the timeout is set to 300ms. Medium
+tests should be less than 1000ms, and the timeout is set to 1500ms. Large tests
+have a timeout of 120000ms, which should cover any remaining tests.
+
+The exception to this rule is when using a runner other than
+[`AndroidJUnitRunner`](https://developer.android.com/training/testing/junit-runner).
+Since these runners do not enforce timeouts, tests that use them must not use a
+size annotation. They will run whenever large tests would run.
+
+Currently the only allowed alternative is the
+[`Parameterized`](https://junit.org/junit4/javadoc/4.12/org/junit/runners/Parameterized.html)
+runner. If you need to use a different runner for some reason, please reach out
+to the team and we can review/approve the use.
+
+#### Disabling tests
+
+To disable a device-side test in presubmit testing only -- but still have it run
+in postsubmit -- use the
+[`@FlakyTest`](https://developer.android.com/reference/androidx/test/filters/FlakyTest)
+annotation. There is currently no support for presubmit-only disabling of
+host-side tests.
+
+If you need to stop a host- or device-side test from running entirely, use
+JUnit's [`@Ignore`](http://junit.sourceforge.net/javadoc/org/junit/Ignore.html)
+annotation. Do *not* use Android's `@Suppress` annotation, which only works with
+Android test runners and will *not* work for host-side tests.
+
+#### Filtering devices
+
+To restrict a test to a range of SDKs, use
+[`@SdkSuppress`](https://developer.android.com/reference/androidx/test/filters/SdkSuppress)
+which allows specifying a range with `minSdkVersion` and `maxSdkVersion`. This
+annotation also supports targeting a specific pre-release SDK with the
+`codeName` parameter.
+
+```java
+// Target SDKs 17 through 19, inclusive
+@SdkSuppress(minSdkVersion = 17, maxSdkVersion = 19)
+
+// Target pre-release SDK R only
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, isCodeName = "R")
+```
+
+You may also gate portions of test implementation code using `SDK_INT` or
+[`BuildCompat.isAtLeast`](https://developer.android.com/reference/androidx/core/os/BuildCompat)
+methods.
+
+To restrict to only phsyical devices, use
+[`@RequiresDevice`](https://developer.android.com/reference/androidx/test/filters/RequiresDevice).
+
+### Animations in tests
+
+Animations are disabled for tests by default. This helps avoid flakes due to
+timing and also makes tests faster.
+
+In rare cases, like testing the animations themselves, you may want to enable
+animations for a particular test or test class. For those cases, you can use the
+[`AnimationDurationScaleRule`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:testutils/testutils-runtime/src/main/java/androidx/testutils/AnimationDurationScaleRule.kt).
+
+## Using the emulator {#emulator}
+
+You can use the emulator or a real device to run tests. If you wish to use the
+emulator, you will need to access the AVD Manager (and your downloaded emulator
+images) using a separate "normal" instance of Android Studio. "Normal" means a
+non-Canary build of Studio that you would use for regular app development -- the
+important part being that it points to the Android SDK where your downloaded
+emulator images reside. You will need to open a project to get the Tools menu --
+do NOT open the AndroidX project in the "normal" instance of Android Studio;
+instead, open a normal app or create a blank project using the app wizard.
+
+## Debugging with platform SDK sources {#sources}
+
+The platform SDK sources that are checked into the development branch may not
+match up with the build of Android present on the emulator or your physical
+device. As a result, the line numbers reported by the debugger may not match up
+the actual code being run.
+
+If you have a copy of the sources for the build against which you are debugging,
+you can manually specify your platform SDK source path:
+
+1.  Click on a module (e.g. `appcompat`) in the `Project` view
+1.  Press `Ctrl-Shift-A` and type "Module Settings", then run the action
+1.  In the `Project Structure` dialog, navigate to `SDKs > Android API 29
+    Platform > Sourcepath`
+1.  Use the `-` button to remove any paths that are present, then use the `+`
+    button to add the desired source path, ex. `<android checkout
+    root>/frameworks/base` if you are debugging against a locally-built system
+    image
+
+NOTE The `Project Structure` dialog reachable via `File > Project Structure` is
+**not** the same as the `Project Structure` dialog that will allow you to
+specify the SDK source path. You must use the "Module Settings" action as
+directed above.
+
+## Running unit and integration tests {#running}
+
+From Android Studio, right-click can be used to run most test targets, including
+source files, classes within a file, or individual test methods but **not**
+entire modules. To run a supported test target, right-click on the test target
+and then click `Run <name of test target>`.
+
+To run tests for an entire module such as `appcompat`, use `Run -> Edit
+configurations...` and use the `+` button to create a new `Android Instrumented
+Tests` configuration. Specify the module to be tested, give it a reasonable name
+(not "All Tests") and click `OK`, then use the `Run` menu to run the
+configuration.
+
+![alt_text](onboarding_images/image2.png "screenshot of run menu")
+
+NOTE If you receive the error `JUnit version 3.8 or later expected` this means
+that Android Studio generated an Android JUnit configuration when you actually
+needed an Android Instrumented Tests configuration. Open the `Run -> Edit
+configurations...` dialog and delete the configuration from Android JUnit, then
+manually add a configuration in Android Instrumented Tests.
+
+### From the command line {#running-from-shell}
+
+Following a successful build, tests may be run against a particular AndroidX
+module using `gradlew`.
+
+To run all integration tests in a specific project, run the following from
+`framework/support`:
+
+```shell
+./gradlew <project-name>:connectedCheck --info --daemon
+```
+
+substituting the Gradle project name (ex. `core`).
+
+To run all integration tests in the specific project and test class you're
+working on, run
+
+```shell
+./gradlew <project-name>:connectedCheck --info --daemon \
+    -Pandroid.testInstrumentationRunnerArguments.class=<fully-qualified-class>[\#testName]
+```
+
+substituting the Gradle project name (ex. `viewpager`) and fully-qualified class
+name (ex. `androidx.viewpager.widget.ViewPagerTest`) of your test file,
+optionally followed by `\#testName` if you want to execute a single test in that
+file.
+
+If you see some weird compilation errors such as below, run `./gradlew clean`
+first:
+
+```
+Unknown source file : UNEXPECTED TOP-LEVEL EXCEPTION:
+Unknown source file : com.android.dex.DexException: Multiple dex files define Landroid/content/pm/ParceledListSlice$1;
+```
+
+## Test apps {#testapps}
+
+Library developers are strongly encouraged to write test apps that exercise
+their library's public API surface. Test apps serve multiple purposes:
+
+*   Integration testing and validation of API testability, when paired with
+    tests
+*   Validation of API usability and developer experience, when paired with a use
+    case or critical user journey
+*   Sample documentation, when embedded into API reference docs using the
+    [`@sample` and `@Sampled` annotations](api_guidelines.md#sample-usage)
+
+### Legacy test apps {#testapps-legacy}
+
+We have a set of legacy sample Android applications in projects suffixed with
+`-demos`. These applications do not have tests and should not be used as test
+apps for new APIs, but they may be useful for manual regression testing.
+
+1.  Click `Run/Debug Configuration` on the top of the window.
+1.  Select the app you want to run.
+1.  Click 'Run' button.
+
+![alt_text](onboarding_images/image3.png "screenshot of Run/Debug menu")
+
+## Benchmarking {#benchmarking}
+
+AndroidX supports benchmarking - locally with Studio/Gradle, and continuously in
+post-submit. For more information on how to create and run benchmarks, see
+[Benchmarking](benchmarking.md).
diff --git a/docs/truth_guide.md b/docs/truth_guide.md
new file mode 100644
index 0000000..fd9efc4
--- /dev/null
+++ b/docs/truth_guide.md
@@ -0,0 +1,147 @@
+# Adding custom Truth subjects
+
+[TOC]
+
+## Custom truth subjects
+
+Every subject class should extend
+[Subject](https://truth.dev/api/latest/com/google/common/truth/Subject.html) and
+follow the naming schema `[ClassUnderTest]Subject`. The Subject must also have a
+constructor that accepts
+[FailureMetadata](https://truth.dev/api/latest/com/google/common/truth/FailureMetadata.html)
+and a reference to the object under test, which are both passed to the
+superclass.
+
+```kotlin
+class NavControllerSubject private constructor(
+    metadata: FailureMetadata,
+    private val actual: NavController
+) : Subject(metadata, actual) { }
+```
+
+### Subject factory
+
+The Subject class should also contain two static fields; a
+[Subject Factory](https://truth.dev/api/latest/com/google/common/truth/Subject.Factory.html)
+and an`assertThat()` shortcut method.
+
+A subject Factory provides most of the functionality of the Subject by allowing
+users to perform all operations provided in the Subject class by passing this
+factory to `about()` methods. E.g:
+
+`assertAbout(navControllers()).that(navController).isGraph(x)` where `isGraph()`
+is a method defined in the Subject class.
+
+The assertThat() shortcut method simply provides a shorthand method for making
+assertions about the Subject without needing a reference to the subject factory.
+i.e., rather than using
+`assertAbout(navControllers()).that(navController).isGraph(x)` users can simply
+use`assertThat(navController).isGraph(x)`.
+
+```kotlin
+companion object {
+    fun navControllers(): Factory<NavControllerSubject, NavController> =
+        Factory<NavControllerSubject, NavController> { metadata, actual ->
+            NavControllerSubject(metadata, actual)
+        }
+
+    @JvmStatic
+    fun assertThat(actual: NavController): NavControllerSubject {
+        return assertAbout(navControllers()).that(actual)
+    }
+}
+```
+
+### Assertion methods
+
+When creating assertion methods for your custom Subject the names of these
+methods should follow the
+[Truth naming convention](https://truth.dev/faq#assertion-naming).
+
+To create assertion methods it is necessary to either delegate to an existing
+assertion method by using `Subject.check()` or to provide your own failure
+strategy by using`failWithActual()` or `failWithoutActual()`.
+
+When using `failWithActual()` the error message will display the`toString()`
+value of the Subject object. Additional information can be added to these error
+messages by using `fact(key, value)` or `simpleFact(value)` where `fact()` will
+be output as a colon separated key, value pair.
+
+```kotlin
+fun isGraph(@IdRes navGraph: Int) {
+    check("graph()").that(actual.graph.id).isEqualTo(navGraph)
+}
+
+// Sample Failure Message
+value of          : navController.graph()
+expected          : 29340
+but was           : 10394
+navController was : {actual.toString() value}
+```
+
+```kotlin
+fun isGraph(@IdRes navGraph: Int) {
+    val actualGraph = actual.graph.id
+    if (actualGraph != navGraph) {
+        failWithoutActual(
+            fact("expected id", navGraph.toString()),
+            fact("but was", actualGraph.toString()),
+            fact("current graph is", actual.graph)
+        )
+    }
+}
+
+// Sample Failure Message
+expected id      : 29340
+but was          : 10394
+current graph is : {actual.graph.toString() value}
+```
+
+## Testing
+
+When testing expected successful assertions it is enough to simply call the
+assertion with verified successful actual and expected values.
+
+```kotlin
+private lateinit var navController: NavController
+@Before
+fun setUp() {
+    navController = NavController(
+        ApplicationProvider.getApplicationContext()
+    ).apply {
+        navigationProvider += TestNavigator()
+        // R.navigation.testGraph == R.id.test_graph
+        setGraph(R.navigation.test_graph)
+    }
+}
+
+@Test
+fun testGraph() {
+    assertThat(navController).isGraph(R.id.test_graph)
+}
+```
+
+To test that expected failure cases you should use the
+[assertThrows](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt)
+function from the AndroidX testutils module. The assertions.kt file contains two
+assertThrows functions. The second method, which specifically returns a
+TruthFailureSubject, should be used since it allows for validating additional
+information about the FailureSubject, particularly that it contains specific
+fact messages.
+
+```kotlin
+@Test
+fun testGraphFailure() {
+    with(assertThrows {
+        assertThat(navController).isGraph(R.id.second_test_graph)
+    }) {
+        factValue("expected id").isEqualTo(R.id.second_test_graph.toString())
+        factValue("but was").isEqualTo(navController.graph.id.toString())
+        factValue("current graph is").isEqualTo(navController.graph.toString())
+    }
+}
+```
+
+## Helpful resources
+
+[Truth extension points](https://truth.dev/extension.html)
diff --git a/docs/versioning.md b/docs/versioning.md
new file mode 100644
index 0000000..f1212a3
--- /dev/null
+++ b/docs/versioning.md
@@ -0,0 +1,319 @@
+# Versioning
+
+[TOC]
+
+## Semantic Versioning
+
+Artifacts follow strict semantic versioning. The version for a finalized release
+will follow the format `<major>.<minor>.<bugfix>` with an optional
+`-<alpha|beta><n>` suffix. Internal or nightly releases should use the
+`-SNAPSHOT` suffix to indicate that the release bits are subject to change.
+
+Also check out the [Versioning FAQ](faq.md#version).
+
+### Major (`x.0.0`) {#major}
+
+An artifact's major version indicates a guaranteed forward-compatibility window.
+For example, a developer could update an artifact versioned `2.0.0` to `2.7.3`
+without taking any additional action.
+
+#### When to increment
+
+An artifact *must* increment its major version number in response to breaking
+changes in binary or behavioral compatibility within the library itself _or_ in
+response to breaking changes within a dependency.
+
+For example, if an artifact updates a SemVer-type dependency from `1.0.0` to
+`2.0.0` then the artifact must also bump its own major version number.
+
+An artifact *may in rare cases* increment its major version number to indicate
+an important but non-breaking change in the library. Note, however, that the
+SemVer implications of incrementing the major version are the same as a breaking
+change -- dependent projects _must_ assume the major version change is breaking
+and update their dependency specifications.
+
+#### Ecosystem implications
+
+When an artifact increases its major version, _all_ artifacts that depended on
+the previous major version are no longer considered compatible and must
+explicitly migrate to depend on the new major version.
+
+As a result, if the library ecosystem is slow to adopt a new major version of an
+artifact then developers may end up in a situation where they cannot update an
+artifact because they depend on a library that has not yet adopted the new major
+version.
+
+For this reason, we *strongly* recommend against increasing the major version of
+a “core” artifact that is depended upon by other libraries. “Leaf” artifacts --
+those that apps depend upon directly and are not used by other libraries -- have
+a much easier time increasing their major version.
+
+#### Process requirements
+
+If the artifact has dependencies within Jetpack, owners *must* complete the
+assessment before implementing any breaking changes to binary or behavioral
+compatibility.
+
+Otherwise, owners are *strongly recommended* to complete the assessment before
+implementing any breaking changes to binary or behavioral compatibility, as such
+changes may negatively impact downstream clients in Android git or Google's
+repository. These clients are not part of our pre-submit workflow, but filling
+out the assessment will provide insight into how they will be affected by a
+major version change.
+
+### Minor (`1.x.0`) {#minor}
+
+Minor indicates compatible public API changes. This number is incremented when
+APIs are added, including the addition of `@Deprecated` annotations. Binary
+compatibility must be preserved between minor version changes.
+
+#### Moving between minor versions:
+
+*   A change in the minor revision indicates the addition of binary-compatible
+    APIs. Libraries **must** increment their minor revision when adding APIs.
+    Dependent libraries are not required to update their minimum required
+    version unless they depend on newly-added APIs.
+
+### Bugfix (`1.0.x`) {#bugfix}
+
+Bugfix indicates internal changes to address broken behavior. Care should be
+taken to ensure that existing clients are not broken, including clients that may
+have been working around long-standing broken behavior.
+
+#### Moving between bugfix versions:
+
+*   A change in the bugfix revision indicates changes in behavior to fix bugs.
+    The API surface does not change. Changes to the bugfix version may *only*
+    occur in a release branch. The bugfix revision must always be `.0` in a
+    development branch.
+
+### Pre-release suffixes {#pre-release-suffix}
+
+The pre-release suffix indicates stability and feature completeness of a
+release. A typical release will begin as alpha, move to beta after acting on
+feedback from internal and external clients, move to release candidate for final
+verification, and ultimately move to a finalized build.
+
+Alpha, beta, and release candidate releases are versioned sequentially using a
+leading zero (ex. alpha01, beta11, rc01) for compatibility with the
+lexicographic ordering of versions used by SemVer.
+
+### Snapshot {#snapshot}
+
+Snapshot releases are whatever exists at tip-of-tree. They are only subject to
+the constraints placed on the average commit. Depending on when it's cut, a
+snapshot may even be binary-identical to an alpha, beta, or stable release.
+
+Versioning policies are enforced by the following Gradle tasks:
+
+`checkApi`: ensures that changes to public API are intentional and tracked,
+asking the developer to explicitly run updateApi (see below) if any changes are
+detected
+
+`checkApiRelease`: verifies that API changes between previously released and
+currently under-development versions conform to semantic versioning guarantees
+
+`updateApi`: commits API changes to source control in such a way that they can
+be reviewed in pre-submit via Gerrit API+1 and reviewed in post-submit by API
+Council
+
+`SNAPSHOT`: is automatically added to the version string for all builds that
+occur outside the build server for release branches (ex. ub-androidx-release).
+Local release builds may be forced by passing -Prelease to the Gradle command
+line.
+
+## Picking the right version {#picking-the-right-version}
+
+AndroidX follows [Strict Semantic Versioning](https://semver.org), which means
+that the version code is strongly tied to the API surface. A full version
+consists of revision numbers for major, minor, and bugfix as well as a
+pre-release stage and revision. Correct versioning is, for the most part,
+automatically enforced; however, please check for the following:
+
+### Initial version {#initial-version}
+
+If your library is brand new, your version should start at 1.0.0, e.g.
+`1.0.0-alpha01`.
+
+The initial release within a new version always starts at `alpha01`. Note the
+two-digit revision code, which allows us to do up to 99 revisions within a
+pre-release stage.
+
+### Pre-release stages
+
+A single version will typically move through several revisions within each of
+the pre-release stages: alpha, beta, rc, and stable. Subsequent revisions within
+a stage (ex. alpha, beta) are incremented by 1, ex. `alpha01` is followed by
+`alpha02` with no gaps.
+
+### Moving between pre-release stages and revisions
+
+Libraries are expected to go through a number of pre-release stages within a
+version prior to release, with stricter requirements at each stage to ensure a
+high-quality stable release. The owner for a library should typically submit a
+CL to update the stage or revision when they are ready to perform a public
+release.
+
+Libraries are expected to allow >= 2 weeks per pre-release stage. This 'soaking
+period' gives developers time to try/use each version, find bugs, and ensure a
+quality stable release. Therefore, at minimum:
+
+-   An `alpha` version must be publically available for 2 weeks before releasing
+    a public `beta`
+-   A `beta` version must be publically available for 2 weeks before releasing
+    an public `rc`
+-   A `rc` version must be publically available fore 2 weeks before releasing a
+    public stable version
+
+Your library must meet the following criteria to move your public release to
+each stage:
+
+### Alpha {#alpha}
+
+Alpha releases are expected to be functionally stable, but may have unstable API
+surface or incomplete features. Typically, alphas have not gone through API
+Council review but are expected to have performed a minimum level of validation.
+
+#### Within the `alphaXX` cycle
+
+*   API surface
+    *   Prior to `alpha01` release, API tracking **must** be enabled (either
+        `publish=true` or create an `api` directory) and remain enabled
+    *   May add/remove APIs within `alpha` cycle, but deprecate/remove cycle is
+        strongly recommended.
+*   Testing
+    *   All changes **should** be accompanied by a `Test:` stanza
+    *   All pre-submit and post-submit tests are passing
+    *   Flaky or failing tests **must** be suppressed or fixed within one day
+        (if affecting pre-submit) or three days (if affecting post-submit)
+
+### Beta {#beta}
+
+Beta releases are ready for production use but may contain bugs. They are
+expected to be functionally stable and have highly-stable, feature-complete API
+surface. APIs should have been reviewed by API Council at this stage, and new
+APIs may only be added with approval by API Council. Tests must have 100%
+coverage of public API surface and translations must be 100% complete.
+
+#### Checklist for moving to `beta01`
+
+*   API surface
+    *   Entire API surface has been reviewed by API Council
+    *   All APIs from alpha undergoing deprecate/remove cycle must be removed
+*   Testing
+    *   All public APIs are tested
+    *   All pre-submit and post-submit tests are enabled (e.g. all suppressions
+        are removed) and passing
+    *   Your library passes `./gradlew library:checkReleaseReady`
+*   No experimental features (e.g. `@UseExperimental`) may be used
+*   All dependencies are `beta`, `rc`, or stable
+*   Be able to answer the question "How will developers test their apps against
+    your library?"
+    *   Ideally, this is an integration app with automated tests that cover the
+        main features of the library and/or a `-testing` artifact as seen in
+        other Jetpack libraries
+
+#### Within the `betaXX` cycle
+
+*   API surface
+    *   New APIs discouraged unless P0 or P1 (ship-blocking)
+    *   May not remove `@Experimental` from experimental APIs, see previous item
+        regarding new APIs
+    *   No API removals allowed
+
+### RC {#rc}
+
+Release candidates are expected to be nearly-identical to the final release, but
+may contain critical last-minute fixes for issues found during integration
+testing.
+
+#### Checklist for moving to `rc01`
+
+*   All previous checklists still apply
+*   Release branch, e.g. `androidx-<group_id>-release`, is created
+*   API surface
+    *   Any API changes from `beta` cycle are reviewed by API Council
+*   No **known** P0 or P1 (ship-blocking) issues
+*   All dependencies are `rc` or stable
+
+#### Within the `rcXX` cycle
+
+*   Ship-blocking bug fixes only
+*   All changes must have corresponding tests
+*   No API changes allowed
+
+### Stable {#stable}
+
+Final releases are well-tested, both by internal tests and external clients, and
+their API surface is reviewed and finalized. While APIs may be deprecated and
+removed in future versions, any APIs added at this stage must remain for at
+least a year.
+
+#### Checklist for moving to stable
+
+*   Identical to a previously released `rcXX` (note that this means any bugs
+    that result in a new release candidate will necessarily delay your stable
+    release by a minimum of two weeks)
+*   No changes of any kind allowed
+*   All dependencies are stable
+
+## Updating your version {#update}
+
+A few notes about version updates:
+
+-   The version of your library listed in `androidx-master-dev` should *always*
+    be higher than the version publically available on Google Maven. This allows
+    us to do proper version tracking and API tracking.
+
+-   Version increments must be done before the CL cutoff date (aka the build cut
+    date).
+
+-   **Increments to the next stability suffix** (like `alpha` to `beta`) should
+    be handled by the library owner, with the Jetpack TPM (nickanthony@) CC'd
+    for API+1.
+
+-   Version increments in release branches will need to follow the guide
+    [How to update your version on a release branch](release_branches.md#update-your-version)
+
+-   When you're ready for `rc01`, the increment to `rc01` should be done in
+    `androidx-master-dev` and then your release branch should be snapped to that
+    build. See the guide [Snap your release branch](release_branches.md#snap) on
+    how to do this. After the release branch is snapped to that build, you will
+    need to update your version in `androidx-master-dev` to `alpha01` of the
+    next minor (or major) version.
+
+### Bi-weekly batched releases (every 2 weeks)
+
+If you participate in a bi-weekly (every 2 weeks) batched release, the Jetpack
+TPM will increment versions for you the day after the build cut deadline. The
+increments are defaulted to increments within the same pre-release suffix.
+
+For example, if you are releasing `1.1.0-alpha04`, the day after the build cut,
+the TPM will increment the version to `1.1.0-alpha05` for the next release.
+
+### How to update your version
+
+1.  Update the version listed in
+    `frameworks/support/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt`
+1.  Run `./gradlew <your-lib>:updateApi`. This will create an API txt file for
+    the new version of your library.
+1.  Verify changes with `./gradlew checkApi verifyDependencyVersions`.
+1.  Commit these change as one commit.
+1.  Upload these changes to Gerrit for review.
+
+An example of a version bump can be found here:
+[aosp/833800](https://android-review.googlesource.com/c/platform/frameworks/support/+/833800)
+
+## `-ktx` Modules {#ktx}
+
+Kotlin Extension modules (`-ktx`) for regular Java modules follow the same
+requirements, but with one exception. They must match the version of the Java
+module that they extend.
+
+For example, let's say you are developing a java library
+`androidx.foo:foo-bar:1.1.0-alpha01` and you want to add a kotlin extension
+module `androidx.foo:foo-bar-ktx` module. Your new `androidx.foo:foo-bar-ktx`
+module will start at version `1.1.0-alpha01` instead of `1.0.0-alpha01`.
+
+If your `androidx.foo:foo-bar` module was in version `1.0.0-alpha06`, then the
+kotlin extension module would start in version `1.0.0-alpha06`.
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 5899803..7e1ec2e 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -70,6 +70,8 @@
 /**
  * Test {@link ExifInterface}.
  */
+// TODO: Add NEF test file from CTS after reducing file size in order to test uncompressed thumbnail
+// image.
 @RunWith(AndroidJUnit4.class)
 public class ExifInterfaceTest {
     private static final String TAG = ExifInterface.class.getSimpleName();
@@ -97,19 +99,20 @@
             "jpeg_with_datetime_tag_primary_format.jpg";
     private static final String JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT =
             "jpeg_with_datetime_tag_secondary_format.jpg";
+    private static final String HEIC_WITH_EXIF = "heic_with_exif.heic";
     private static final int[] IMAGE_RESOURCES = new int[] {
             R.raw.jpeg_with_exif_byte_order_ii, R.raw.jpeg_with_exif_byte_order_mm,
             R.raw.dng_with_exif_with_xmp, R.raw.jpeg_with_exif_with_xmp,
             R.raw.png_with_exif_byte_order_ii, R.raw.png_without_exif, R.raw.webp_with_exif,
             R.raw.webp_with_anim_without_exif, R.raw.webp_without_exif,
             R.raw.webp_lossless_without_exif, R.raw.jpeg_with_datetime_tag_primary_format,
-            R.raw.jpeg_with_datetime_tag_secondary_format};
+            R.raw.jpeg_with_datetime_tag_secondary_format, R.raw.heic_with_exif};
     private static final String[] IMAGE_FILENAMES = new String[] {
             JPEG_WITH_EXIF_BYTE_ORDER_II, JPEG_WITH_EXIF_BYTE_ORDER_MM, DNG_WITH_EXIF_WITH_XMP,
             JPEG_WITH_EXIF_WITH_XMP, PNG_WITH_EXIF_BYTE_ORDER_II, PNG_WITHOUT_EXIF,
             WEBP_WITH_EXIF, WEBP_WITHOUT_EXIF_WITH_ANIM_DATA, WEBP_WITHOUT_EXIF,
             WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING, JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT,
-            JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT};
+            JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT, HEIC_WITH_EXIF};
 
     private static final int USER_READ_WRITE = 0600;
     private static final String TEST_TEMP_FILE_NAME = "testImage";
@@ -455,6 +458,19 @@
         writeToFilesWithoutExif(WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING);
     }
 
+    /**
+     * .heic file is a container for HEIF format images, which ExifInterface supports.
+     */
+    @Test
+    @LargeTest
+    public void testHeicFile() throws Throwable {
+        // TODO: Reading HEIC file for SDK < 28 throws an exception. Revisit once issue is solved.
+        //  (b/172025296)
+        if (Build.VERSION.SDK_INT > 27) {
+            readFromFilesWithExif(HEIC_WITH_EXIF, R.array.heic_with_exif);
+        }
+    }
+
     @Test
     @LargeTest
     public void testDoNotFailOnCorruptedImage() throws Throwable {
@@ -628,22 +644,32 @@
         }
     }
 
+    /**
+     * JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT contains the following tags:
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "2016:01:29 18:32:27"
+     *   TAG_OFFSET_TIME, TAG_OFFSET_TIME_ORIGINAL, TAG_OFFSET_TIME_DIGITIZED = "100000"
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "+09:00"
+     */
     @Test
     @SmallTest
     public void testGetSetDateTime() throws IOException {
-        final long expectedDatetimeValue = 1454059947000L;
-        final long expectedDatetimeOffsetLongValue = 32400000L; /* +09:00 converted to msec */
+        final long expectedGetDatetimeValue =
+                1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+                + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+        // GPS datetime does not support subsec precision
+        final long expectedGetGpsDatetimeValue =
+                1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
         final String expectedDatetimeOffsetStringValue = "+09:00";
-        final String dateTimeValue = "2017:02:02 22:22:22";
-        final String dateTimeOriginalValue = "2017:01:01 11:11:11";
 
         File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
         ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(expectedDatetimeValue, (long) exif.getDateTime());
-        assertEquals(expectedDatetimeValue, (long) exif.getDateTimeOriginal());
-        assertEquals(expectedDatetimeValue, (long) exif.getDateTimeDigitized());
-        assertEquals(expectedDatetimeValue, (long) exif.getGpsDateTime());
-        // getDateTime() = TAG_DATETIME + TAG_OFFSET_TIME
+        // Test getting datetime values
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTime());
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTimeOriginal());
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTimeDigitized());
+        assertEquals(expectedGetGpsDatetimeValue, (long) exif.getGpsDateTime());
         assertEquals(expectedDatetimeOffsetStringValue,
                 exif.getAttribute(ExifInterface.TAG_OFFSET_TIME));
         assertEquals(expectedDatetimeOffsetStringValue,
@@ -651,28 +677,29 @@
         assertEquals(expectedDatetimeOffsetStringValue,
                 exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED));
 
-        exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
-        exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
-        exif.saveAttributes();
-
-        // Check that the DATETIME value is not overwritten by DATETIME_ORIGINAL's value.
-        exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
-        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
-
-        // Now remove the DATETIME value.
-        exif.setAttribute(ExifInterface.TAG_DATETIME, null);
-        exif.saveAttributes();
-
-        // When the DATETIME has no value, then it should be set to DATETIME_ORIGINAL's value.
-        exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
-
-        long currentTimeStamp = System.currentTimeMillis();
+        // Test setting datetime values
+        final long currentTimeStamp = System.currentTimeMillis();
+        final long expectedDatetimeOffsetLongValue = 32400000L;
         exif.setDateTime(currentTimeStamp);
         exif.saveAttributes();
         exif = new ExifInterface(imageFile.getAbsolutePath());
         assertEquals(currentTimeStamp - expectedDatetimeOffsetLongValue, (long) exif.getDateTime());
+
+        // Test that setting null throws NPE
+        try {
+            exif.setDateTime(null);
+            fail();
+        } catch (NullPointerException e) {
+            // Expected
+        }
+
+        // Test that setting negative value throws IAE
+        try {
+            exif.setDateTime(-1L);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
     }
 
     /**
@@ -685,30 +712,141 @@
      * Setting a datetime tag value with the secondary format with
      * {@link ExifInterface#setAttribute(String, String)} should automatically convert it to the
      * primary format.
+     *
+     * JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT contains the following tags:
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "2016:01:29 18:32:27"
+     *   TAG_OFFSET_TIME, TAG_OFFSET_TIME_ORIGINAL, TAG_OFFSET_TIME_DIGITIZED = "100000"
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "+09:00"
      */
     @Test
     @SmallTest
     public void testGetSetDateTimeForSecondaryFormat() throws Exception {
-        final long dateTimePrimaryFormatLongValue = 1604075491000L;
-        final String dateTimePrimaryFormatStringValue = "2020-10-30 16:31:31";
+        // Test getting datetime values
+        final long expectedGetDatetimeValue =
+                1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+                + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+        final String expectedDateTimeStringValue = "2016-01-29 18:32:27";
+
         File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT);
-
-        // Check that secondary format value is read correctly.
         ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(dateTimePrimaryFormatStringValue,
+        assertEquals(expectedDateTimeStringValue,
                 exif.getAttribute(ExifInterface.TAG_DATETIME));
-        assertEquals(dateTimePrimaryFormatLongValue, (long) exif.getDateTime());
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTime());
 
-        final long dateTimeSecondaryFormatLongValue = 1577836800000L;
-        final String dateTimeSecondaryFormatStringValue = "2020-01-01 00:00:00";
-        final String modifiedDateTimeSecondaryFormatStringValue = "2020:01:01 00:00:00";
+        // Test setting datetime value: check that secondary format value is modified correctly
+        // when it is saved.
+        final long newDateTimeLongValue =
+                1577772000000L /* TAG_DATETIME value ("2020-01-01 00:00:00") converted to msec */
+                + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+        final String newDateTimeStringValue = "2020-01-01 00:00:00";
+        final String modifiedNewDateTimeStringValue = "2020:01:01 00:00:00";
 
-        // Check that secondary format value is written correctly.
-        exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeSecondaryFormatStringValue);
+        exif.setAttribute(ExifInterface.TAG_DATETIME, newDateTimeStringValue);
         exif.saveAttributes();
-        assertEquals(modifiedDateTimeSecondaryFormatStringValue,
-                exif.getAttribute(ExifInterface.TAG_DATETIME));
-        assertEquals(dateTimeSecondaryFormatLongValue, (long) exif.getDateTime());
+        assertEquals(modifiedNewDateTimeStringValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+        assertEquals(newDateTimeLongValue, (long) exif.getDateTime());
+    }
+
+    @Test
+    @LargeTest
+    public void testAddDefaultValuesForCompatibility() throws Exception {
+        File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+
+        // 1. Check that the TAG_DATETIME value is not overwritten by TAG_DATETIME_ORIGINAL's value
+        // when TAG_DATETIME value exists.
+        final String dateTimeValue = "2017:02:02 22:22:22";
+        final String dateTimeOriginalValue = "2017:01:01 11:11:11";
+        exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
+        exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
+        exif.saveAttributes();
+        exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+
+        // 2. Check that when TAG_DATETIME has no value, it is set to TAG_DATETIME_ORIGINAL's value.
+        exif.setAttribute(ExifInterface.TAG_DATETIME, null);
+        exif.saveAttributes();
+        exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+    }
+
+    @Test
+    @LargeTest
+    public void testSubsec() throws IOException {
+        File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+
+        // Set initial value to 0
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 0ms */ "000");
+        exif.saveAttributes();
+        assertEquals("000", exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+        long currentDateTimeValue = exif.getDateTime();
+
+        // Test that single and double-digit values are set properly.
+        // Note that since SubSecTime tag records fractions of a second, a single-digit value
+        // should be counted as the first decimal value, which is why "1" becomes 100ms and "11"
+        // becomes 110ms.
+        String oneDigitSubSec = "1";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, oneDigitSubSec);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 100, (long) exif.getDateTime());
+        assertEquals(oneDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        String twoDigitSubSec1 = "01";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, twoDigitSubSec1);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
+        assertEquals(twoDigitSubSec1, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        String twoDigitSubSec2 = "11";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, twoDigitSubSec2);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 110, (long) exif.getDateTime());
+        assertEquals(twoDigitSubSec2, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        // Test that 3-digit values are set properly.
+        String hundredMs = "100";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, hundredMs);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 100, (long) exif.getDateTime());
+        assertEquals(hundredMs, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        // Test that values starting with zero are also supported.
+        String oneMsStartingWithZeroes = "001";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, oneMsStartingWithZeroes);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 1, (long) exif.getDateTime());
+        assertEquals(oneMsStartingWithZeroes, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        String tenMsStartingWithZero = "010";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, tenMsStartingWithZero);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
+        assertEquals(tenMsStartingWithZero, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        // Test that values with more than three digits are set properly. getAttribute() should
+        // return the whole string, but getDateTime() should only add the first three digits
+        // because it supports only up to 1/1000th of a second.
+        String fourDigitSubSec = "1234";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, fourDigitSubSec);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 123, (long) exif.getDateTime());
+        assertEquals(fourDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        String fiveDigitSubSec = "23456";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, fiveDigitSubSec);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 234, (long) exif.getDateTime());
+        assertEquals(fiveDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+        String sixDigitSubSec = "345678";
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, sixDigitSubSec);
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 345, (long) exif.getDateTime());
+        assertEquals(sixDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
     }
 
     @Test
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic b/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic
new file mode 100644
index 0000000..ec03bfd
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
index f50e845..5930e2c 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
index 53ad4e9..9899556 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
index e6f342e..b3438c9 100644
--- a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
+++ b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
@@ -367,4 +367,48 @@
         <item>0</item>
         <item>0</item>
     </array>
+    <array name="heic_with_exif">
+        <!--Whether thumbnail exists-->
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+        <item>0</item>
+        <item>0</item>
+        <item>false</item>
+        <!--Whether GPS LatLong information exists-->
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <!--Whether Make information exists-->
+        <item>true</item>
+        <item>3519</item>
+        <item>4</item>
+        <item>LGE</item>
+        <item>Nexus 5</item>
+        <item>0.0</item>
+        <item />
+        <item>0.0</item>
+        <item>0</item>
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item>1080</item>
+        <item>1920</item>
+        <item />
+        <item>1</item>
+        <item>0</item>
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+    </array>
 </resources>
\ No newline at end of file
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index c0347c4..4099177 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -16,6 +16,13 @@
 
 package androidx.exifinterface.media;
 
+import static androidx.exifinterface.media.ExifInterfaceUtils.byteArrayToHexString;
+import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray;
+import static androidx.exifinterface.media.ExifInterfaceUtils.copy;
+import static androidx.exifinterface.media.ExifInterfaceUtils.isSupportedFormatForSavingAttributes;
+import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds;
+import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith;
+
 import android.annotation.SuppressLint;
 import android.content.res.AssetManager;
 import android.graphics.Bitmap;
@@ -3835,21 +3842,21 @@
     static final byte MARKER_EOI = (byte) 0xd9;
 
     // Supported Image File Types
-    private static final int IMAGE_TYPE_UNKNOWN = 0;
-    private static final int IMAGE_TYPE_ARW = 1;
-    private static final int IMAGE_TYPE_CR2 = 2;
-    private static final int IMAGE_TYPE_DNG = 3;
-    private static final int IMAGE_TYPE_JPEG = 4;
-    private static final int IMAGE_TYPE_NEF = 5;
-    private static final int IMAGE_TYPE_NRW = 6;
-    private static final int IMAGE_TYPE_ORF = 7;
-    private static final int IMAGE_TYPE_PEF = 8;
-    private static final int IMAGE_TYPE_RAF = 9;
-    private static final int IMAGE_TYPE_RW2 = 10;
-    private static final int IMAGE_TYPE_SRW = 11;
-    private static final int IMAGE_TYPE_HEIF = 12;
-    private static final int IMAGE_TYPE_PNG = 13;
-    private static final int IMAGE_TYPE_WEBP = 14;
+    static final int IMAGE_TYPE_UNKNOWN = 0;
+    static final int IMAGE_TYPE_ARW = 1;
+    static final int IMAGE_TYPE_CR2 = 2;
+    static final int IMAGE_TYPE_DNG = 3;
+    static final int IMAGE_TYPE_JPEG = 4;
+    static final int IMAGE_TYPE_NEF = 5;
+    static final int IMAGE_TYPE_NRW = 6;
+    static final int IMAGE_TYPE_ORF = 7;
+    static final int IMAGE_TYPE_PEF = 8;
+    static final int IMAGE_TYPE_RAF = 9;
+    static final int IMAGE_TYPE_RW2 = 10;
+    static final int IMAGE_TYPE_SRW = 11;
+    static final int IMAGE_TYPE_HEIF = 12;
+    static final int IMAGE_TYPE_PNG = 13;
+    static final int IMAGE_TYPE_WEBP = 14;
 
     static {
         sFormatterPrimary = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
@@ -4694,7 +4701,7 @@
      * </p>
      */
     public void saveAttributes() throws IOException {
-        if (!isSupportedFormatForSavingAttributes()) {
+        if (!isSupportedFormatForSavingAttributes(mMimeType)) {
             throw new IOException("ExifInterface only supports saving attributes on JPEG, PNG, "
                     + "or WebP formats.");
         }
@@ -5144,14 +5151,30 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     public void setDateTime(@NonNull Long timeStamp) {
-        long sub = timeStamp % 1000;
+        if (timeStamp == null) {
+            throw new NullPointerException("Timestamp should not be null.");
+        }
+
+        if (timeStamp < 0) {
+            throw new IllegalArgumentException("Timestamp should a positive value.");
+        }
+
+        long subsec = timeStamp % 1000;
+        String subsecString = Long.toString(subsec);
+        for (int i = subsecString.length(); i < 3; i++) {
+            subsecString = "0" + subsecString;
+        }
         setAttribute(TAG_DATETIME, sFormatterPrimary.format(new Date(timeStamp)));
-        setAttribute(TAG_SUBSEC_TIME, Long.toString(sub));
+        setAttribute(TAG_SUBSEC_TIME, subsecString);
     }
 
     /**
      * Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
      * Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
+     *
      * @return null if date time information is unavailable or invalid.
      *
      * @hide
@@ -5167,6 +5190,10 @@
     /**
      * Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
      * milliseconds since Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
+     *
      * @return null if digitized date time information is unavailable or invalid.
      *
      * @hide
@@ -5182,6 +5209,10 @@
     /**
      * Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
      * milliseconds since Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
+     *
      * @return null if original date time information is unavailable or invalid.
      *
      * @hide
@@ -5224,15 +5255,7 @@
             }
 
             if (subSecs != null) {
-                try {
-                    long sub = Long.parseLong(subSecs);
-                    while (sub > 1000) {
-                        sub /= 10;
-                    }
-                    msecs += sub;
-                } catch (NumberFormatException e) {
-                    // Ignored
-                }
+                msecs += parseSubSeconds(subSecs);
             }
             return msecs;
         } catch (IllegalArgumentException e) {
@@ -8073,87 +8096,4 @@
             Log.e(TAG, "closeFileDescriptor is called in API < 21, which must be wrong.");
         }
     }
-
-    /**
-     * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
-     * Returns the total number of bytes transferred.
-     */
-    private static int copy(InputStream in, OutputStream out) throws IOException {
-        int total = 0;
-        byte[] buffer = new byte[8192];
-        int c;
-        while ((c = in.read(buffer)) != -1) {
-            total += c;
-            out.write(buffer, 0, c);
-        }
-        return total;
-    }
-
-    /**
-     * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
-     * closed.
-     */
-    private static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
-        int remainder = numBytes;
-        byte[] buffer = new byte[8192];
-        while (remainder > 0) {
-            int bytesToRead = Math.min(remainder, 8192);
-            int bytesRead = in.read(buffer, 0, bytesToRead);
-            if (bytesRead != bytesToRead) {
-                throw new IOException("Failed to copy the given amount of bytes from the input"
-                        + "stream to the output stream.");
-            }
-            remainder -= bytesRead;
-            out.write(buffer, 0, bytesRead);
-        }
-    }
-
-    /**
-     * Convert given int[] to long[]. If long[] is given, just return it.
-     * Return null for other types of input.
-     */
-    private static long[] convertToLongArray(Object inputObj) {
-        if (inputObj instanceof int[]) {
-            int[] input = (int[]) inputObj;
-            long[] result = new long[input.length];
-            for (int i = 0; i < input.length; i++) {
-                result[i] = input[i];
-            }
-            return result;
-        } else if (inputObj instanceof long[]) {
-            return (long[]) inputObj;
-        }
-        return null;
-    }
-
-    private static boolean startsWith(byte[] cur, byte[] val) {
-        if (cur == null || val == null) {
-            return false;
-        }
-        if (cur.length < val.length) {
-            return false;
-        }
-        for (int i = 0; i < val.length; i++) {
-            if (cur[i] != val[i]) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static String byteArrayToHexString(byte[] bytes) {
-        StringBuilder sb = new StringBuilder(bytes.length * 2);
-        for (int i = 0; i < bytes.length; i++) {
-            sb.append(String.format("%02x", bytes[i]));
-        }
-        return sb.toString();
-    }
-
-    private boolean isSupportedFormatForSavingAttributes() {
-        if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_PNG
-                || mMimeType == IMAGE_TYPE_WEBP) {
-            return true;
-        }
-        return false;
-    }
 }
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
new file mode 100644
index 0000000..8c3390d
--- /dev/null
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.exifinterface.media;
+
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_JPEG;
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_PNG;
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_WEBP;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class ExifInterfaceUtils {
+    private static final String TAG = "ExifInterfaceUtils";
+
+    private ExifInterfaceUtils() {
+        // Prevent instantiation
+    }
+    /**
+     * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
+     * Returns the total number of bytes transferred.
+     */
+    static int copy(InputStream in, OutputStream out) throws IOException {
+        int total = 0;
+        byte[] buffer = new byte[8192];
+        int c;
+        while ((c = in.read(buffer)) != -1) {
+            total += c;
+            out.write(buffer, 0, c);
+        }
+        return total;
+    }
+
+    /**
+     * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
+     * closed.
+     */
+    static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
+        int remainder = numBytes;
+        byte[] buffer = new byte[8192];
+        while (remainder > 0) {
+            int bytesToRead = Math.min(remainder, 8192);
+            int bytesRead = in.read(buffer, 0, bytesToRead);
+            if (bytesRead != bytesToRead) {
+                throw new IOException("Failed to copy the given amount of bytes from the input"
+                        + "stream to the output stream.");
+            }
+            remainder -= bytesRead;
+            out.write(buffer, 0, bytesRead);
+        }
+    }
+
+    /**
+     * Convert given int[] to long[]. If long[] is given, just return it.
+     * Return null for other types of input.
+     */
+    static long[] convertToLongArray(Object inputObj) {
+        if (inputObj instanceof int[]) {
+            int[] input = (int[]) inputObj;
+            long[] result = new long[input.length];
+            for (int i = 0; i < input.length; i++) {
+                result[i] = input[i];
+            }
+            return result;
+        } else if (inputObj instanceof long[]) {
+            return (long[]) inputObj;
+        }
+        return null;
+    }
+
+    static boolean startsWith(byte[] cur, byte[] val) {
+        if (cur == null || val == null) {
+            return false;
+        }
+        if (cur.length < val.length) {
+            return false;
+        }
+        for (int i = 0; i < val.length; i++) {
+            if (cur[i] != val[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static boolean isSupportedFormatForSavingAttributes(int mimeType) {
+        if (mimeType == IMAGE_TYPE_JPEG || mimeType == IMAGE_TYPE_PNG
+                || mimeType == IMAGE_TYPE_WEBP) {
+            return true;
+        }
+        return false;
+    }
+
+    static String byteArrayToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder(bytes.length * 2);
+        for (int i = 0; i < bytes.length; i++) {
+            sb.append(String.format("%02x", bytes[i]));
+        }
+        return sb.toString();
+    }
+
+    static long parseSubSeconds(String subSec) {
+        try {
+            final int len = Math.min(subSec.length(), 3);
+            long sub = Long.parseLong(subSec.substring(0, len));
+            for (int i = len; i < 3; i++) {
+                sub *= 10;
+            }
+            return sub;
+        } catch (NumberFormatException e) {
+            // Ignored
+        }
+        return 0L;
+    }
+}
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 6cfa8e7..88435eb 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -36,11 +36,11 @@
     api("androidx.collection:collection-ktx:1.1.0") {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-livedata-core-ktx")) {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01")
-    api("androidx.savedstate:savedstate-ktx:1.1.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
+    api(projectOrArtifact(":savedstate:savedstate-ktx")) {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
     api(KOTLIN_STDLIB)
diff --git a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
index 0674e06..a2735f9 100644
--- a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
+++ b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
@@ -228,7 +228,7 @@
  * If your testing Fragment has a dependency to specific theme such as `Theme.AppCompat`,
  * use the theme ID parameter in [launch] method.
  *
- * @param <F> The Fragment class being tested
+ * @param F The Fragment class being tested
  *
  * @see ActivityScenario a scenario API for Activity
  */
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 961ddb2..a6c2996 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -38,10 +38,10 @@
     api("androidx.viewpager:viewpager:1.0.0")
     api("androidx.loader:loader:1.0.0")
     api(projectOrArtifact(":activity:activity"))
-    api("androidx.lifecycle:lifecycle-livedata-core:2.3.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.3.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
-    api("androidx.savedstate:savedstate:1.1.0-beta01")
+    api(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+    api(projectOrArtifact(":savedstate:savedstate"))
     api("androidx.annotation:annotation-experimental:1.0.0")
 
     androidTestImplementation("androidx.appcompat:appcompat:1.1.0", {
diff --git a/fragment/fragment/src/androidTest/AndroidManifest.xml b/fragment/fragment/src/androidTest/AndroidManifest.xml
index 5bac34a..97ee537 100644
--- a/fragment/fragment/src/androidTest/AndroidManifest.xml
+++ b/fragment/fragment/src/androidTest/AndroidManifest.xml
@@ -48,6 +48,7 @@
         <activity android:name="androidx.fragment.app.FragmentSavedStateActivity" />
         <activity android:name="androidx.fragment.app.FragmentFinishEarlyTestActivity" />
         <activity android:name="androidx.fragment.app.SimpleContainerActivity" />
+        <activity android:name="androidx.fragment.app.ReplaceInCreateActivity" />
         <activity android:name="androidx.fragment.app.DialogActivity"
                   android:theme="@style/DialogTheme"/>
         <activity android:name="androidx.fragment.app.TestAppCompatActivity"
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
index c10b6d9..c5706ec 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
@@ -755,6 +755,50 @@
         }
     }
 
+    @Test
+    fun removingFragmentAnimationChange() {
+        waitForAnimationReady()
+        val fm = activityRule.activity.supportFragmentManager
+
+        val fragment1 = AnimationFragment()
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .add(R.id.fragmentContainer, fragment1, "fragment1")
+            .addToBackStack("fragment1")
+            .commit()
+        activityRule.waitForExecution()
+
+        val fragment2 = AnimationFragment()
+
+        fm.beginTransaction()
+            .setReorderingAllowed(true)
+            .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
+            .replace(R.id.fragmentContainer, fragment2, "fragment2")
+            .addToBackStack("fragment2")
+            .commit()
+
+        activityRule.waitForExecution()
+
+        activityRule.runOnUiThread {
+            fm.popBackStack("fragment1", 0)
+
+            val fragment3 = AnimationFragment()
+
+            fm.beginTransaction()
+                .setReorderingAllowed(true)
+                .setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
+                .replace(R.id.fragmentContainer, fragment3, "fragment3")
+                .addToBackStack("fragment3")
+                .commit()
+        }
+
+        activityRule.waitForExecution()
+
+        activityRule.runOnUiThread {
+            assertThat(fragment2.loadedAnimation).isEqualTo(EXIT)
+        }
+    }
+
     private fun assertEnterPopExit(fragment: AnimationFragment) {
         assertFragmentAnimation(fragment, 1, true, ENTER)
 
@@ -842,6 +886,7 @@
         var animation: Animation? = null
         var enter: Boolean = false
         var resourceId: Int = 0
+        var loadedAnimation = 0
 
         override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
             if (nextAnim == 0 ||
@@ -849,6 +894,7 @@
             ) {
                 return null
             }
+            loadedAnimation = nextAnim
             numAnimators++
             animation = TranslateAnimation(-10f, 0f, 0f, 0f)
             (animation as TranslateAnimation).duration = 1
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
index d23e658..38b7570 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
@@ -19,6 +19,7 @@
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
+import android.os.Bundle
 import android.view.View
 import android.view.ViewGroup
 import android.widget.EditText
@@ -71,6 +72,31 @@
         }
     }
 
+    @Test
+    fun focusedViewRootView() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fragment = RequestViewFragment()
+
+            withActivity {
+                setContentView(R.layout.simple_container)
+
+                supportFragmentManager.beginTransaction()
+                    .setCustomAnimations(1, 0)
+                    .replace(R.id.fragmentContainer, fragment)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+            }
+
+            assertThat(fragment.endAnimationCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+                .isTrue()
+
+            withActivity {
+                val view = fragment.requireView()
+                assertThat(view.isFocused).isTrue()
+            }
+        }
+    }
+
     class RemoveEditViewFragment : StrictViewFragment(R.layout.with_edit_text) {
         val endAnimationCountDownLatch = CountDownLatch(1)
         override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator? {
@@ -95,4 +121,28 @@
             return animator
         }
     }
+
+    class RequestViewFragment : StrictViewFragment() {
+        val endAnimationCountDownLatch = CountDownLatch(1)
+        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+            super.onViewCreated(view, savedInstanceState)
+            view.isFocusable = true
+            view.isFocusableInTouchMode = true
+            view.requestFocus()
+        }
+
+        override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator? {
+            if (nextAnim == 0) {
+                return null
+            }
+
+            val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1)
+            animator.addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    endAnimationCountDownLatch.countDown()
+                }
+            })
+            return animator
+        }
+    }
 }
\ No newline at end of file
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
index 4c155a7..f10ddfc 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
@@ -15,60 +15,121 @@
  */
 package androidx.fragment.app
 
+import android.os.Bundle
 import android.view.View
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
+import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
+import androidx.test.filters.LargeTest
+import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 /**
- * Test to prevent regressions in SupportFragmentManager fragment replace method. See b/24693644
+ * Test to prevent regressions in SupportFragmentManager fragment replace method.
  */
 @RunWith(AndroidJUnit4::class)
-@MediumTest
+@LargeTest
 class FragmentReplaceTest {
-    @Suppress("DEPRECATION")
-    @get:Rule
-    val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
     @Test
     fun testReplaceFragment() {
-        val activity = activityRule.activity
-        val fm = activity.supportFragmentManager
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fm = withActivity { supportFragmentManager }
 
-        fm.beginTransaction()
-            .add(R.id.content, StrictViewFragment(R.layout.fragment_a))
-            .addToBackStack(null)
-            .commit()
-        executePendingTransactions(fm)
-        assertThat(activity.findViewById<View>(R.id.textA)).isNotNull()
-        assertThat(activity.findViewById<View>(R.id.textB)).isNull()
-        assertThat(activity.findViewById<View>(R.id.textC)).isNull()
+            fm.beginTransaction()
+                .add(R.id.content, StrictViewFragment(R.layout.fragment_a))
+                .addToBackStack(null)
+                .commit()
+            executePendingTransactions()
+            withActivity {
+                assertThat(findViewById<View>(R.id.textA)).isNotNull()
+                assertThat(findViewById<View>(R.id.textB)).isNull()
+                assertThat(findViewById<View>(R.id.textC)).isNull()
+            }
 
-        fm.beginTransaction()
-            .add(R.id.content, StrictViewFragment(R.layout.fragment_b))
-            .addToBackStack(null)
-            .commit()
-        executePendingTransactions(fm)
-        assertThat(activity.findViewById<View>(R.id.textA)).isNotNull()
-        assertThat(activity.findViewById<View>(R.id.textB)).isNotNull()
-        assertThat(activity.findViewById<View>(R.id.textC)).isNull()
+            fm.beginTransaction()
+                .add(R.id.content, StrictViewFragment(R.layout.fragment_b))
+                .addToBackStack(null)
+                .commit()
+            executePendingTransactions()
+            withActivity {
+                assertThat(findViewById<View>(R.id.textA)).isNotNull()
+                assertThat(findViewById<View>(R.id.textB)).isNotNull()
+                assertThat(findViewById<View>(R.id.textC)).isNull()
+            }
 
-        activity.supportFragmentManager.beginTransaction()
-            .replace(R.id.content, StrictViewFragment(R.layout.fragment_c))
-            .addToBackStack(null)
-            .commit()
-        executePendingTransactions(fm)
-        assertThat(activity.findViewById<View>(R.id.textA)).isNull()
-        assertThat(activity.findViewById<View>(R.id.textB)).isNull()
-        assertThat(activity.findViewById<View>(R.id.textC)).isNotNull()
+            fm.beginTransaction()
+                .replace(R.id.content, StrictViewFragment(R.layout.fragment_c))
+                .addToBackStack(null)
+                .commit()
+            executePendingTransactions()
+            withActivity {
+                assertThat(findViewById<View>(R.id.textA)).isNull()
+                assertThat(findViewById<View>(R.id.textB)).isNull()
+                assertThat(findViewById<View>(R.id.textC)).isNotNull()
+            }
+        }
     }
 
-    private fun executePendingTransactions(fm: FragmentManager) {
-        activityRule.runOnUiThread { fm.executePendingTransactions() }
+    @Test
+    fun testReplaceFragmentInOnCreate() {
+        with(ActivityScenario.launch(ReplaceInCreateActivity::class.java)) {
+            val replaceInCreateFragment = withActivity { this.replaceInCreateFragment }
+
+            assertThat(replaceInCreateFragment.isAdded)
+                .isFalse()
+            withActivity {
+                assertThat(findViewById<View>(R.id.textA)).isNull()
+                assertThat(findViewById<View>(R.id.textB)).isNotNull()
+            }
+        }
+    }
+}
+
+class ReplaceInCreateActivity : FragmentActivity(R.layout.activity_content) {
+    private val parentFragment: ParentFragment
+        get() = supportFragmentManager.findFragmentById(R.id.content) as ParentFragment
+    val replaceInCreateFragment: ReplaceInCreateFragment
+        get() = parentFragment.replaceInCreateFragment
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (savedInstanceState == null) {
+            // This issue only appears for child fragments
+            // so add parent fragment that contains the ReplaceInCreateFragment
+            supportFragmentManager.beginTransaction()
+                .add(R.id.content, ParentFragment())
+                .setReorderingAllowed(true)
+                .commit()
+        }
+    }
+
+    class ParentFragment : StrictViewFragment(R.layout.simple_container) {
+        lateinit var replaceInCreateFragment: ReplaceInCreateFragment
+
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            if (savedInstanceState == null) {
+                replaceInCreateFragment = ReplaceInCreateFragment()
+                childFragmentManager.beginTransaction()
+                    .add(R.id.fragmentContainer, replaceInCreateFragment)
+                    .setReorderingAllowed(true)
+                    .commit()
+            }
+        }
+    }
+}
+
+class ReplaceInCreateFragment : StrictViewFragment(R.layout.fragment_a) {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        parentFragmentManager.beginTransaction()
+            .replace(R.id.fragmentContainer, StrictViewFragment(R.layout.fragment_b))
+            .setReorderingAllowed(true)
+            .commit()
     }
 }
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
index 1775a46..1776faf 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
@@ -42,7 +42,7 @@
     @Test
     fun fragmentWithOptionsMenu() {
         activityRule.setContentView(R.layout.simple_container)
-        val fragment = OptionsMenuFragment()
+        val fragment = MenuFragment()
         val fm = activityRule.activity.supportFragmentManager
         fm.beginTransaction()
             .add(R.id.fragmentContainer, fragment)
@@ -112,6 +112,30 @@
     }
 
     @Test
+    fun childFragmentWithPrepareOptionsMenuParentMenuVisibilityFalse() {
+        activityRule.setContentView(R.layout.simple_container)
+        val parent = ParentOptionsMenuFragment()
+        val fm = activityRule.activity.supportFragmentManager
+
+        parent.setMenuVisibility(false)
+        fm.beginTransaction()
+            .add(R.id.fragmentContainer, parent, "parent")
+            .commit()
+        activityRule.executePendingTransactions()
+
+        assertWithMessage("Fragment should not have an options menu")
+            .that(parent.hasOptionsMenu()).isFalse()
+        assertWithMessage("Child fragment should have an options menu")
+            .that(parent.childFragmentManager.checkForMenus()).isTrue()
+
+        activityRule.runOnUiThread {
+            assertWithMessage("child fragment onCreateOptions menu was not called")
+                .that(parent.childFragment.onPrepareOptionsMenuCountDownLatch.count)
+                .isEqualTo(1)
+        }
+    }
+
+    @Test
     fun parentAndChildFragmentWithOptionsMenu() {
         activityRule.setContentView(R.layout.simple_container)
         val parent = ParentOptionsMenuFragment(true)
@@ -165,7 +189,7 @@
         activityRule.executePendingTransactions()
 
         parent.childFragmentManager.beginTransaction()
-            .add(R.id.fragmentContainer2, OptionsMenuFragment())
+            .add(R.id.fragmentContainer2, MenuFragment())
             .commit()
         activityRule.executePendingTransactions()
 
@@ -175,8 +199,9 @@
             .that(parent.mChildFragmentManager.checkForMenus()).isTrue()
     }
 
-    class OptionsMenuFragment : StrictViewFragment(R.layout.fragment_a) {
+    class MenuFragment : StrictViewFragment(R.layout.fragment_a) {
         val onCreateOptionsMenuCountDownLatch = CountDownLatch(1)
+        val onPrepareOptionsMenuCountDownLatch = CountDownLatch(1)
 
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
@@ -188,12 +213,17 @@
             onCreateOptionsMenuCountDownLatch.countDown()
             inflater.inflate(R.menu.example_menu, menu)
         }
+
+        override fun onPrepareOptionsMenu(menu: Menu) {
+            super.onPrepareOptionsMenu(menu)
+            onPrepareOptionsMenuCountDownLatch.countDown()
+        }
     }
 
     class ParentOptionsMenuFragment(
         val createMenu: Boolean = false
     ) : StrictViewFragment(R.layout.double_container) {
-        val childFragment = OptionsMenuFragment()
+        val childFragment = MenuFragment()
 
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
index 0720e07..256a07b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
@@ -51,6 +51,7 @@
         checkGetActivity()
         checkActivityNotDestroyed()
         checkState("onViewCreated", State.CREATED)
+        currentState = State.VIEW_CREATED
         onViewCreatedCalled = true
     }
 
@@ -78,7 +79,8 @@
                 .isNotNull()
         }
         checkGetActivity()
-        checkState("onDestroyView", State.CREATED)
+        checkState("onDestroyView", State.CREATED, State.VIEW_CREATED, State.ACTIVITY_CREATED)
+        currentState = State.CREATED
         onDestroyViewCalled = true
     }
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java
index cd15968..84142fb 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.java
@@ -369,7 +369,7 @@
      * @param v {@link View} that might be added to list of disappearing views
      */
     private void addDisappearingFragmentView(@NonNull View v) {
-        if (v.getAnimation() != null || (mTransitioningFragmentViews != null
+        if (v.getAnimation() != null && (mTransitioningFragmentViews != null
                 && mTransitioningFragmentViews.contains(v))) {
             if (mDisappearingFragmentChildren == null) {
                 mDisappearingFragmentChildren = new ArrayList<>();
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 6445a872..f3e9d37 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -2402,7 +2402,7 @@
      */
     private void setVisibleRemovingFragment(@NonNull Fragment f) {
         ViewGroup container = getFragmentContainer(f);
-        if (container != null && f.mView != null) {
+        if (container != null && f.getNextAnim() > 0) {
             if (container.getTag(R.id.visible_removing_fragment_view_tag) == null) {
                 container.setTag(R.id.visible_removing_fragment_view_tag, f);
             }
@@ -3193,7 +3193,7 @@
         boolean show = false;
         for (Fragment f : mFragmentStore.getFragments()) {
             if (f != null) {
-                if (f.performPrepareOptionsMenu(menu)) {
+                if (isParentMenuVisible(f) && f.performPrepareOptionsMenu(menu)) {
                     show = true;
                 }
             }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
index 03c6948..4446391 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
@@ -247,6 +247,10 @@
         if (mFragment.mDeferStart && mFragment.mState < Fragment.STARTED) {
             maxState = Math.min(maxState, Fragment.ACTIVITY_CREATED);
         }
+        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+            Log.v(FragmentManager.TAG, "computeExpectedState() of " + maxState + " for "
+                    + mFragment);
+        }
         return maxState;
     }
 
@@ -335,10 +339,11 @@
                             mFragment.mState = Fragment.AWAITING_EXIT_EFFECTS;
                             break;
                         case Fragment.VIEW_CREATED:
-                            destroyFragmentView();
+                            mFragment.mInLayout = false;
                             mFragment.mState = Fragment.VIEW_CREATED;
                             break;
                         case Fragment.CREATED:
+                            destroyFragmentView();
                             mFragment.mState = Fragment.CREATED;
                             break;
                         case Fragment.ATTACHED:
@@ -604,6 +609,9 @@
     }
 
     private boolean isFragmentViewChild(@NonNull View view) {
+        if (view == mFragment.mView) {
+            return true;
+        }
         ViewParent parent = view.getParent();
         while (parent != null) {
             if (parent == mFragment.mView) {
@@ -722,6 +730,12 @@
         if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
             Log.d(TAG, "movefrom CREATE_VIEW: " + mFragment);
         }
+        // In cases where we never got up to AWAITING_EXIT_EFFECTS, we
+        // need to manually remove the view from the container to reverse
+        // what we did in createView()
+        if (mFragment.mContainer != null && mFragment.mView != null) {
+            mFragment.mContainer.removeView(mFragment.mView);
+        }
         mFragment.performDestroyView();
         mDispatcher.dispatchOnFragmentViewDestroyed(mFragment, false);
         mFragment.mContainer = null;
diff --git a/inspection/inspection-gradle-plugin/build.gradle b/inspection/inspection-gradle-plugin/build.gradle
index aff9915..b09f862 100644
--- a/inspection/inspection-gradle-plugin/build.gradle
+++ b/inspection/inspection-gradle-plugin/build.gradle
@@ -36,7 +36,7 @@
     implementation(AGP_STABLE)
     implementation(KOTLIN_STDLIB)
     implementation("gradle.plugin.com.google.protobuf:protobuf-gradle-plugin:0.8.13")
-    implementation("org.anarres.jarjar:jarjar-gradle:1.0.1")
+    implementation("com.github.jengelman.gradle.plugins:shadow:5.2.0")
 
     testImplementation(project(":internal-testutils-gradle-plugin"))
     testImplementation gradleTestKit()
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
index e8370d5..b10b84b 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
@@ -19,7 +19,6 @@
 import com.android.build.gradle.BaseExtension
 import com.android.build.gradle.api.BaseVariant
 import com.android.build.gradle.api.LibraryVariant
-import org.anarres.gradle.plugin.jarjar.JarjarTask
 import org.gradle.api.DefaultTask
 import org.gradle.api.Project
 import org.gradle.api.file.ConfigurableFileCollection
@@ -30,6 +29,7 @@
 import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.TaskAction
 import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
 import java.io.File
 
 abstract class DexInspectorTask : DefaultTask() {
@@ -73,13 +73,13 @@
 fun Project.registerDexInspectorTask(
     variant: BaseVariant,
     extension: BaseExtension,
-    jarJar: TaskProvider<JarjarTask>
+    jar: TaskProvider<out Jar>
 ): TaskProvider<DexInspectorTask> {
     return tasks.register(variant.taskName("dexInspector"), DexInspectorTask::class.java) {
         it.setDx(extension.sdkDirectory, extension.buildToolsVersion)
-        it.jars.from(jarJar.get().destinationPath)
+        it.jars.from(jar.get().destinationDirectory)
         val out = File(taskWorkingDir(variant, "dexedInspector"), "${project.name}.jar")
         it.outputFile.set(out)
-        it.dependsOn(jarJar)
+        it.dependsOn(jar)
     }
 }
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
index 7751681..e5a1eba 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
@@ -52,8 +52,8 @@
                 if (variant.name == "release") {
                     foundReleaseVariant = true
                     val unzip = project.registerUnzipTask(variant)
-                    val jarJar = project.registerJarJarDependenciesTask(variant, unzip)
-                    dexTask = project.registerDexInspectorTask(variant, libExtension, jarJar)
+                    val shadowJar = project.registerShadowDependenciesTask(variant, unzip)
+                    dexTask = project.registerDexInspectorTask(variant, libExtension, shadowJar)
                 }
             }
             libExtension.sourceSets.findByName("main")!!.resources.srcDirs(
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/JarJarDependenciesTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
similarity index 84%
rename from inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/JarJarDependenciesTask.kt
rename to inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
index 33b6ca2b..14893cb 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/JarJarDependenciesTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
@@ -17,7 +17,7 @@
 package androidx.inspection.gradle
 
 import com.android.build.gradle.api.BaseVariant
-import org.anarres.gradle.plugin.jarjar.JarjarTask
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
 import org.gradle.api.Project
 import org.gradle.api.tasks.Copy
 import org.gradle.api.tasks.TaskProvider
@@ -27,29 +27,29 @@
 
 // variant.taskName relies on @ExperimentalStdlibApi api
 @ExperimentalStdlibApi
-fun Project.registerJarJarDependenciesTask(
+fun Project.registerShadowDependenciesTask(
     variant: BaseVariant,
     zipTask: TaskProvider<Copy>
-): TaskProvider<JarjarTask> {
+): TaskProvider<ShadowJar> {
     val uberJar = registerUberJarTask(variant)
     return tasks.register(
-        variant.taskName("jarJarDependencies"),
-        JarjarTask::class.java
+        variant.taskName("shadowDependencies"),
+        ShadowJar::class.java
     ) {
         it.dependsOn(uberJar)
         val fileTree = project.fileTree(zipTask.get().destinationDir)
         fileTree.include("**/*.jar")
         it.from(fileTree)
-        it.destinationDir = taskWorkingDir(variant, "jarJar")
-        it.destinationName = "${project.name}-shadowed.jar"
+        it.destinationDirectory.set(taskWorkingDir(variant, "shadowedJar"))
+        it.archiveBaseName.set("${project.name}-shadowed")
         it.dependsOn(zipTask)
         val prefix = "deps.${project.name.replace('-', '.')}"
         it.doFirst {
-            val task = it as JarjarTask
+            val task = it as ShadowJar
             val input = uberJar.get().outputs.files
             task.from(input)
             input.extractPackageNames().forEach { packageName ->
-                task.classRename("$packageName.**", "$prefix.$packageName.@1")
+                task.relocate(packageName, "$prefix.$packageName")
             }
         }
     }
diff --git a/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt b/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
index 167669e..ab97f41 100644
--- a/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
+++ b/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
@@ -42,7 +42,7 @@
             isRequired = false
         )
 
-        private fun createOption(
+        internal fun createOption(
             argName: String,
             desc: String,
             isRequired: Boolean = true,
@@ -94,4 +94,4 @@
 
 fun main(args: Array<String>) {
     Main().run(args)
-}
\ No newline at end of file
+}
diff --git a/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt b/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt
index df0666f..71dafe2 100644
--- a/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt
+++ b/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt
@@ -64,9 +64,9 @@
 
         val inputFile = File(javaClass.getResource(inputZipPath).file)
 
-        @Suppress("DEPRECATION")
+        @Suppress("DEPRECATION") // b/174695914
         val tempDir = createTempDir()
-        @Suppress("DEPRECATION")
+        @Suppress("DEPRECATION") // b/174695914
         val expectedFile = File(createTempDir(), "test.zip")
 
         @Suppress("deprecation")
@@ -103,9 +103,9 @@
 
         val inputFile = File(javaClass.getResource(inputZipPath).file)
 
-        @Suppress("DEPRECATION")
+        @Suppress("DEPRECATION") // b/174695914
         val tempDir = createTempDir()
-        @Suppress("DEPRECATION")
+        @Suppress("DEPRECATION") // b/174695914
         val expectedFile = File(createTempDir(), "test.zip")
 
         @Suppress("deprecation")
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
index 587c1ce..af71151 100644
--- a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
+++ b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
@@ -102,7 +102,7 @@
             isRequired = false
         )
 
-        private fun createOption(
+        internal fun createOption(
             argName: String,
             argNameLong: String,
             desc: String,
@@ -166,7 +166,7 @@
 
         val fileMappings = mutableSetOf<FileMapping>()
         if (rebuildTopOfTree) {
-            @Suppress("DEPRECATION")
+            @Suppress("DEPRECATION") // b/174695914
             val tempFile = createTempFile(suffix = "zip")
             fileMappings.add(FileMapping(input, tempFile))
         } else {
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 7a7e046..63486ac 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -12,8 +12,8 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api(project(":leanback:leanback"))
-    api("androidx.paging:paging-runtime:3.0.0-alpha09")
+    api("androidx.leanback:leanback:1.1.0-beta01")
+    api(project(":paging:paging-runtime"))
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
index 85e2137..7e4b192 100644
--- a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
+++ b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
@@ -19,14 +19,12 @@
 import androidx.paging.CombinedLoadStates
 import androidx.paging.ExperimentalPagingApi
 import androidx.paging.LoadState
-import androidx.paging.LoadType
 import androidx.paging.Pager
 import androidx.paging.PagingConfig
 import androidx.paging.PagingData
 import androidx.paging.TestPagingSource
 import androidx.paging.assertEvents
 import androidx.paging.localLoadStatesOf
-import androidx.paging.toCombinedLoadStatesLocal
 import androidx.recyclerview.widget.DiffUtil
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -139,15 +137,18 @@
         // empty previous list.
         assertEvents(
             listOf(
-                LoadType.REFRESH to LoadState.Loading,
-                LoadType.REFRESH to LoadState.NotLoading(endOfPaginationReached = false)
-            ).toCombinedLoadStatesLocal(),
+                localLoadStatesOf(),
+                localLoadStatesOf(refreshLocal = LoadState.Loading),
+                localLoadStatesOf(
+                    refreshLocal = LoadState.NotLoading(endOfPaginationReached = false)
+                ),
+            ),
             loadEvents
         )
         loadEvents.clear()
         job.cancel()
 
-        pagingDataAdapter.submitData(TestLifecycleOwner().lifecycle, PagingData.empty<Int>())
+        pagingDataAdapter.submitData(TestLifecycleOwner().lifecycle, PagingData.empty())
         advanceUntilIdle()
         // Assert that all load state updates are sent, even when differ enters fast path for
         // empty next list.
diff --git a/leanback/leanback-preference/build.gradle b/leanback/leanback-preference/build.gradle
index caedc44..81faf4c 100644
--- a/leanback/leanback-preference/build.gradle
+++ b/leanback/leanback-preference/build.gradle
@@ -13,7 +13,7 @@
     api("androidx.appcompat:appcompat:1.0.0")
     api("androidx.recyclerview:recyclerview:1.0.0")
     api("androidx.preference:preference:1.1.0")
-    api(project(":leanback:leanback"))
+    api("androidx.leanback:leanback:1.1.0-beta01")
 }
 
 android {
diff --git a/leanback/leanback-tab/build.gradle b/leanback/leanback-tab/build.gradle
index d58b65a..4481a37 100644
--- a/leanback/leanback-tab/build.gradle
+++ b/leanback/leanback-tab/build.gradle
@@ -46,7 +46,7 @@
 androidx {
     name = "AndroidX Leanback Tab"
     publish = Publish.SNAPSHOT_AND_RELEASE
-    mavenVersion = LibraryVersions.LEANBACK
+    mavenVersion = LibraryVersions.LEANBACK_TAB
     mavenGroup = LibraryGroups.LEANBACK
     inceptionYear = "2020"
     description = "This library adds top tab navigation component to be used in TV"
diff --git a/leanback/leanback/api/1.1.0-beta01.txt b/leanback/leanback/api/1.1.0-beta01.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/1.1.0-beta01.txt
+++ b/leanback/leanback/api/1.1.0-beta01.txt
@@ -883,6 +883,7 @@
     method @Deprecated public void onCreate(android.os.Bundle!);
     method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
     method @Deprecated public void onDestroy();
+    method @Deprecated public void onDestroyView();
     method @Deprecated public void onPause();
     method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
     method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/current.txt b/leanback/leanback/api/current.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/current.txt
+++ b/leanback/leanback/api/current.txt
@@ -883,6 +883,7 @@
     method @Deprecated public void onCreate(android.os.Bundle!);
     method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
     method @Deprecated public void onDestroy();
+    method @Deprecated public void onDestroyView();
     method @Deprecated public void onPause();
     method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
     method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt b/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt
+++ b/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt
@@ -883,6 +883,7 @@
     method @Deprecated public void onCreate(android.os.Bundle!);
     method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
     method @Deprecated public void onDestroy();
+    method @Deprecated public void onDestroyView();
     method @Deprecated public void onPause();
     method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
     method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/public_plus_experimental_current.txt b/leanback/leanback/api/public_plus_experimental_current.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/public_plus_experimental_current.txt
+++ b/leanback/leanback/api/public_plus_experimental_current.txt
@@ -883,6 +883,7 @@
     method @Deprecated public void onCreate(android.os.Bundle!);
     method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
     method @Deprecated public void onDestroy();
+    method @Deprecated public void onDestroyView();
     method @Deprecated public void onPause();
     method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
     method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/restricted_1.1.0-beta01.txt b/leanback/leanback/api/restricted_1.1.0-beta01.txt
index 981b38c..65d5941 100644
--- a/leanback/leanback/api/restricted_1.1.0-beta01.txt
+++ b/leanback/leanback/api/restricted_1.1.0-beta01.txt
@@ -924,6 +924,7 @@
     method @Deprecated public void onCreate(android.os.Bundle!);
     method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
     method @Deprecated public void onDestroy();
+    method @Deprecated public void onDestroyView();
     method @Deprecated public void onPause();
     method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
     method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/restricted_current.txt b/leanback/leanback/api/restricted_current.txt
index 981b38c..65d5941 100644
--- a/leanback/leanback/api/restricted_current.txt
+++ b/leanback/leanback/api/restricted_current.txt
@@ -924,6 +924,7 @@
     method @Deprecated public void onCreate(android.os.Bundle!);
     method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
     method @Deprecated public void onDestroy();
+    method @Deprecated public void onDestroyView();
     method @Deprecated public void onPause();
     method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
     method @Deprecated public void onResume();
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index df6b059f..8e03560 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -15,7 +15,7 @@
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.media:media:1.0.0")
     api("androidx.fragment:fragment:1.0.0")
-    api project(":recyclerview:recyclerview")
+    api("androidx.recyclerview:recyclerview:1.2.0-beta01")
     api("androidx.appcompat:appcompat:1.0.0")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/leanback/leanback/src/androidTest/generatev4.py b/leanback/leanback/src/androidTest/generatev4.py
index c540b2a..d05788b 100755
--- a/leanback/leanback/src/androidTest/generatev4.py
+++ b/leanback/leanback/src/androidTest/generatev4.py
@@ -75,7 +75,8 @@
 
 ####### generate XXXFragmentTest classes #######
 
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows', 'Headers']
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows',
+'Headers', 'Search']
 
 for w in testcls:
     print "copy {}SupporFragmentTest to {}FragmentTest".format(w, w)
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTest.java
index 747c63f..afc30c9 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepFragmentTest.java
@@ -133,8 +133,6 @@
 
         sendKey(KeyEvent.KEYCODE_BACK);
         PollingCheck.waitFor(new PollingCheck.ActivityStop(activity));
-        verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
-        assertTrue(activity.isDestroyed());
     }
 
     @Test
@@ -371,7 +369,6 @@
 
         sendKey(KeyEvent.KEYCODE_BACK);
         PollingCheck.waitFor(new PollingCheck.ActivityStop(activity));
-        verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
     }
 
     @Test
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepSupportFragmentTest.java
index eef8c0a..450c145 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/GuidedStepSupportFragmentTest.java
@@ -130,8 +130,6 @@
 
         sendKey(KeyEvent.KEYCODE_BACK);
         PollingCheck.waitFor(new PollingCheck.ActivityStop(activity));
-        verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
-        assertTrue(activity.isDestroyed());
     }
 
     @Test
@@ -368,7 +366,6 @@
 
         sendKey(KeyEvent.KEYCODE_BACK);
         PollingCheck.waitFor(new PollingCheck.ActivityStop(activity));
-        verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
     }
 
     @Test
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
new file mode 100644
index 0000000..0565f5f
--- /dev/null
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
@@ -0,0 +1,157 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SearchSupportFragmentTest.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.leanback.app;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import android.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.ListRowPresenter;
+import androidx.leanback.widget.ObjectAdapter;
+import androidx.leanback.widget.VerticalGridView;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.testutils.AnimationTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@AnimationTest
+@RunWith(AndroidJUnit4.class)
+public class SearchFragmentTest extends SingleFragmentTestBase {
+
+    static final StringPresenter CARD_PRESENTER = new StringPresenter();
+
+    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(CARD_PRESENTER);
+            int index = 0;
+            for (int j = 0; j < repeatPerRow; ++j) {
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("This is a test-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("GuidedStepFragment-" + (index++));
+            }
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+    }
+
+    public static final class F_LeakFragment extends SearchFragment
+            implements SearchFragment.SearchResultProvider {
+        ArrayObjectAdapter mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            loadData(mRowsAdapter, 10, 1);
+        }
+
+        @Override
+        public ObjectAdapter getResultsAdapter() {
+            return mRowsAdapter;
+        }
+
+        @Override
+        public boolean onQueryTextChange(String newQuery) {
+            return true;
+        }
+
+        @Override
+        public boolean onQueryTextSubmit(String query) {
+            return true;
+        }
+    }
+
+    public static final class EmptyFragment extends Fragment {
+        EditText mEditText;
+
+        @Override
+        public View onCreateView(
+                final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            return mEditText = new EditText(container.getContext());
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            // focus IME on the new fragment because there is a memory leak that IME remembers
+            // last editable view, which will cause a false reporting of leaking View.
+            InputMethodManager imm =
+                    (InputMethodManager) getActivity()
+                            .getSystemService(Context.INPUT_METHOD_SERVICE);
+            mEditText.requestFocus();
+            imm.showSoftInput(mEditText, 0);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mEditText = null;
+            super.onDestroyView();
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+    @Test
+    public void viewLeakTest() throws Throwable {
+        SingleFragmentTestActivity activity = launchAndWaitActivity(F_LeakFragment.class,
+                1000);
+
+        VerticalGridView gridView = ((SearchFragment) activity.getTestFragment())
+                .getRowsFragment().getVerticalGridView();
+        LeakDetector leakDetector = new LeakDetector();
+        leakDetector.observeObject(gridView);
+        leakDetector.observeObject(gridView.getRecycledViewPool());
+        for (int i = 0; i < gridView.getChildCount(); i++) {
+            leakDetector.observeObject(gridView.getChildAt(i));
+        }
+        gridView = null;
+        EmptyFragment emptyFragment = new EmptyFragment();
+        activity.getFragmentManager().beginTransaction()
+                .replace(R.id.main_frame, emptyFragment)
+                .addToBackStack("BK")
+                .commit();
+
+        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return emptyFragment.isResumed();
+            }
+        });
+        leakDetector.assertNoLeak();
+    }
+}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
new file mode 100644
index 0000000..446400b
--- /dev/null
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.leanback.app;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import androidx.fragment.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.ListRowPresenter;
+import androidx.leanback.widget.ObjectAdapter;
+import androidx.leanback.widget.VerticalGridView;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.testutils.AnimationTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@AnimationTest
+@RunWith(AndroidJUnit4.class)
+public class SearchSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+    static final StringPresenter CARD_PRESENTER = new StringPresenter();
+
+    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(CARD_PRESENTER);
+            int index = 0;
+            for (int j = 0; j < repeatPerRow; ++j) {
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("This is a test-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
+            }
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+    }
+
+    public static final class F_LeakFragment extends SearchSupportFragment
+            implements SearchSupportFragment.SearchResultProvider {
+        ArrayObjectAdapter mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            loadData(mRowsAdapter, 10, 1);
+        }
+
+        @Override
+        public ObjectAdapter getResultsAdapter() {
+            return mRowsAdapter;
+        }
+
+        @Override
+        public boolean onQueryTextChange(String newQuery) {
+            return true;
+        }
+
+        @Override
+        public boolean onQueryTextSubmit(String query) {
+            return true;
+        }
+    }
+
+    public static final class EmptyFragment extends Fragment {
+        EditText mEditText;
+
+        @Override
+        public View onCreateView(
+                final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            return mEditText = new EditText(container.getContext());
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            // focus IME on the new fragment because there is a memory leak that IME remembers
+            // last editable view, which will cause a false reporting of leaking View.
+            InputMethodManager imm =
+                    (InputMethodManager) getActivity()
+                            .getSystemService(Context.INPUT_METHOD_SERVICE);
+            mEditText.requestFocus();
+            imm.showSoftInput(mEditText, 0);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mEditText = null;
+            super.onDestroyView();
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+    @Test
+    public void viewLeakTest() throws Throwable {
+        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_LeakFragment.class,
+                1000);
+
+        VerticalGridView gridView = ((SearchSupportFragment) activity.getTestFragment())
+                .getRowsSupportFragment().getVerticalGridView();
+        LeakDetector leakDetector = new LeakDetector();
+        leakDetector.observeObject(gridView);
+        leakDetector.observeObject(gridView.getRecycledViewPool());
+        for (int i = 0; i < gridView.getChildCount(); i++) {
+            leakDetector.observeObject(gridView.getChildAt(i));
+        }
+        gridView = null;
+        EmptyFragment emptyFragment = new EmptyFragment();
+        activity.getSupportFragmentManager().beginTransaction()
+                .replace(R.id.main_frame, emptyFragment)
+                .addToBackStack("BK")
+                .commit();
+
+        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return emptyFragment.isResumed();
+            }
+        });
+        leakDetector.assertNoLeak();
+    }
+}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
index 21cc610..644405a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
@@ -424,6 +424,13 @@
     }
 
     @Override
+    public void onDestroyView() {
+        mSearchBar = null;
+        mRowsFragment = null;
+        super.onDestroyView();
+    }
+
+    @Override
     public void onDestroy() {
         releaseAdapter();
         super.onDestroy();
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
index b28c4ec..124da0b 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
@@ -419,6 +419,13 @@
     }
 
     @Override
+    public void onDestroyView() {
+        mSearchBar = null;
+        mRowsSupportFragment = null;
+        super.onDestroyView();
+    }
+
+    @Override
     public void onDestroy() {
         releaseAdapter();
         super.onDestroy();
diff --git a/lifecycle/lifecycle-service/build.gradle b/lifecycle/lifecycle-service/build.gradle
index 1f4f5dff..2b81a01 100644
--- a/lifecycle/lifecycle-service/build.gradle
+++ b/lifecycle/lifecycle-service/build.gradle
@@ -31,7 +31,7 @@
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
-    androidTestImplementation("androidx.legacy:legacy-support-core-utils:1.0.0")
+    androidTestImplementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
 }
 
 androidx {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
index 314443b..8e3a3fb 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.lifecycle.viewmodel.savedstate
 
+import android.os.Bundle
 import androidx.annotation.MainThread
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.SavedStateHandle
@@ -169,9 +170,11 @@
     fun testKeySet() {
         val accessor = SavedStateHandle()
         accessor.set("s", "pb")
+        accessor.getLiveData<String>("no value ld")
         accessor.getLiveData<String>("ld").value = "a"
-        assertThat(accessor.keys().size).isEqualTo(2)
-        assertThat(accessor.keys()).containsExactly("s", "ld")
+        accessor.setSavedStateProvider("provider") { Bundle() }
+        assertThat(accessor.keys().size).isEqualTo(4)
+        assertThat(accessor.keys()).containsExactly("s", "ld", "provider", "no value ld")
     }
 
     @MainThread
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
index 146eb52..af17aa1 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
@@ -32,8 +32,8 @@
 
 import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -217,11 +217,17 @@
 
     /**
      * Returns all keys contained in this {@link SavedStateHandle}
+     * <p>
+     * Returned set contains all keys: keys used to get LiveData-s, to set SavedStateProviders and
+     * keys used in regular {@link #set(String, Object)}.
      */
     @MainThread
     @NonNull
     public Set<String> keys() {
-        return Collections.unmodifiableSet(mRegular.keySet());
+        HashSet<String> allKeys = new HashSet<>(mRegular.keySet());
+        allKeys.addAll(mSavedStateProviders.keySet());
+        allKeys.addAll(mLiveDatas.keySet());
+        return allKeys;
     }
 
     /**
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
index 13b478b..c581d23 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -35,8 +35,7 @@
  * finished.
  * <p>
  * In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a
- * configuration change (e.g. rotation). The new instance of the owner will just re-connected to the
- * existing ViewModel.
+ * configuration change (e.g. rotation). The new owner instance just re-connects to the existing model.
  * <p>
  * The purpose of the ViewModel is to acquire and keep the information that is necessary for an
  * Activity or a Fragment. The Activity or the Fragment should be able to observe changes in the
diff --git a/media/media/api/current.txt b/media/media/api/current.txt
index 983f283..ff8752c 100644
--- a/media/media/api/current.txt
+++ b/media/media/api/current.txt
@@ -446,6 +446,7 @@
     field public static final long ACTION_REWIND = 8L; // 0x8L
     field public static final long ACTION_SEEK_TO = 256L; // 0x100L
     field public static final long ACTION_SET_CAPTIONING_ENABLED = 1048576L; // 0x100000L
+    field public static final long ACTION_SET_PLAYBACK_SPEED = 4194304L; // 0x400000L
     field public static final long ACTION_SET_RATING = 128L; // 0x80L
     field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
     field public static final long ACTION_SET_SHUFFLE_MODE = 2097152L; // 0x200000L
diff --git a/media/media/api/public_plus_experimental_current.txt b/media/media/api/public_plus_experimental_current.txt
index 77263b5..a6eaa11 100644
--- a/media/media/api/public_plus_experimental_current.txt
+++ b/media/media/api/public_plus_experimental_current.txt
@@ -446,6 +446,7 @@
     field public static final long ACTION_REWIND = 8L; // 0x8L
     field public static final long ACTION_SEEK_TO = 256L; // 0x100L
     field public static final long ACTION_SET_CAPTIONING_ENABLED = 1048576L; // 0x100000L
+    field public static final long ACTION_SET_PLAYBACK_SPEED = 4194304L; // 0x400000L
     field public static final long ACTION_SET_RATING = 128L; // 0x80L
     field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
     field public static final long ACTION_SET_SHUFFLE_MODE = 2097152L; // 0x200000L
diff --git a/media/media/api/restricted_current.txt b/media/media/api/restricted_current.txt
index b22a5f6..052b25c 100644
--- a/media/media/api/restricted_current.txt
+++ b/media/media/api/restricted_current.txt
@@ -457,6 +457,7 @@
     field public static final long ACTION_REWIND = 8L; // 0x8L
     field public static final long ACTION_SEEK_TO = 256L; // 0x100L
     field public static final long ACTION_SET_CAPTIONING_ENABLED = 1048576L; // 0x100000L
+    field public static final long ACTION_SET_PLAYBACK_SPEED = 4194304L; // 0x400000L
     field public static final long ACTION_SET_RATING = 128L; // 0x80L
     field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
     field public static final long ACTION_SET_SHUFFLE_MODE = 2097152L; // 0x200000L
@@ -502,7 +503,7 @@
     field public static final int STATE_STOPPED = 1; // 0x1
   }
 
-  @LongDef(flag=true, value={android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP, android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY, android.support.v4.media.session.PlaybackStateCompat.ACTION_REWIND, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT, android.support.v4.media.session.PlaybackStateCompat.ACTION_FAST_FORWARD, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_RATING, android.support.v4.media.session.PlaybackStateCompat.ACTION_SEEK_TO, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_REPEAT_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PlaybackStateCompat.Actions {
+  @LongDef(flag=true, value={android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP, android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY, android.support.v4.media.session.PlaybackStateCompat.ACTION_REWIND, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT, android.support.v4.media.session.PlaybackStateCompat.ACTION_FAST_FORWARD, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_RATING, android.support.v4.media.session.PlaybackStateCompat.ACTION_SEEK_TO, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_REPEAT_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PlaybackStateCompat.Actions {
   }
 
   public static final class PlaybackStateCompat.Builder {
diff --git a/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java b/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
index 40d565a..d51a003 100644
--- a/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -55,7 +55,8 @@
             ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
             ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
             ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI,
-            ACTION_SET_REPEAT_MODE, ACTION_SET_SHUFFLE_MODE, ACTION_SET_CAPTIONING_ENABLED})
+            ACTION_SET_REPEAT_MODE, ACTION_SET_SHUFFLE_MODE, ACTION_SET_CAPTIONING_ENABLED,
+            ACTION_SET_PLAYBACK_SPEED})
     @Retention(RetentionPolicy.SOURCE)
     public @interface Actions {}
 
@@ -225,6 +226,13 @@
     public static final long ACTION_SET_SHUFFLE_MODE = 1 << 21;
 
     /**
+     * Indicates this session supports the set playback speed command.
+     *
+     * @see Builder#setActions(long)
+     */
+    public static final long ACTION_SET_PLAYBACK_SPEED = 1 << 22;
+
+    /**
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP_PREFIX) // used by media2-session
@@ -717,6 +725,7 @@
      * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
      * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}</li>
      * <li> {@link PlaybackStateCompat#ACTION_SET_CAPTIONING_ENABLED}</li>
+     * <li> {@link PlaybackStateCompat#ACTION_SET_PLAYBACK_SPEED}</li>
      * </ul>
      */
     @Actions
@@ -1255,6 +1264,7 @@
          * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
          * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}</li>
          * <li> {@link PlaybackStateCompat#ACTION_SET_CAPTIONING_ENABLED}</li>
+         * <li> {@link PlaybackStateCompat#ACTION_SET_PLAYBACK_SPEED}</li>
          * </ul>
          *
          * @return this
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java
index 6cb6393..353cadc 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java
@@ -284,6 +284,42 @@
         parcel.recycle();
     }
 
+    /**
+     * Tests that each ACTION_* constant does not overlap.
+     */
+    @Test
+    @SmallTest
+    public void testActionConstantDoesNotOverlap() {
+        long[] actionConstants = new long[] {
+                PlaybackStateCompat.ACTION_STOP, PlaybackStateCompat.ACTION_PAUSE,
+                PlaybackStateCompat.ACTION_PLAY, PlaybackStateCompat.ACTION_REWIND,
+                PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
+                PlaybackStateCompat.ACTION_SKIP_TO_NEXT,
+                PlaybackStateCompat.ACTION_FAST_FORWARD,
+                PlaybackStateCompat.ACTION_SET_RATING,
+                PlaybackStateCompat.ACTION_SEEK_TO,
+                PlaybackStateCompat.ACTION_PLAY_PAUSE,
+                PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID,
+                PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH,
+                PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM,
+                PlaybackStateCompat.ACTION_PLAY_FROM_URI,
+                PlaybackStateCompat.ACTION_PREPARE,
+                PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID,
+                PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH,
+                PlaybackStateCompat.ACTION_PREPARE_FROM_URI,
+                PlaybackStateCompat.ACTION_SET_REPEAT_MODE,
+                PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE,
+                PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED,
+                PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED};
+
+        // Check that the values are not overlapped.
+        for (int i = 0; i < actionConstants.length; i++) {
+            for (int j = i + 1; j < actionConstants.length; j++) {
+                assertEquals(0, actionConstants[i] & actionConstants[j]);
+            }
+        }
+    }
+
     private void assertCustomActionEquals(PlaybackStateCompat.CustomAction action1,
             PlaybackStateCompat.CustomAction action2) {
         assertEquals(action1.getAction(), action2.getAction());
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java b/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java
index de2971e..07df689 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java
@@ -122,6 +122,34 @@
         mServiceInstance.startForeground(id, notification);
     }
 
+    /**
+     * Updates the notification when needed.
+     * This will be called when the current media item is changed.
+     */
+    @Override
+    public void onNotificationUpdateNeeded(MediaSession session) {
+        MediaSessionService.MediaNotification mediaNotification =
+                mServiceInstance.onUpdateNotification(session);
+        if (mediaNotification == null) {
+            // The service implementation doesn't want to use the automatic start/stopForeground
+            // feature.
+            return;
+        }
+
+        int id = mediaNotification.getNotificationId();
+        Notification notification = mediaNotification.getNotification();
+
+        if (Build.VERSION.SDK_INT >= 21) {
+            // Call Notification.MediaStyle#setMediaSession() indirectly.
+            android.media.session.MediaSession.Token fwkToken =
+                    (android.media.session.MediaSession.Token)
+                            session.getSessionCompat().getSessionToken().getToken();
+            notification.extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, fwkToken);
+        }
+
+        mNotificationManager.notify(id, notification);
+    }
+
     @Override
     public void onSessionClosed(MediaSession session) {
         mServiceInstance.removeSession(session);
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSession.java b/media2/session/src/main/java/androidx/media2/session/MediaSession.java
index f6447861..5ba8b5d 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSession.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSession.java
@@ -787,6 +787,12 @@
             }
         }
 
+        final void onCurrentMediaItemChanged(MediaSession session) {
+            if (mForegroundServiceEventCallback != null) {
+                mForegroundServiceEventCallback.onNotificationUpdateNeeded(session);
+            }
+        }
+
         final void onSessionClosed(MediaSession session) {
             if (mForegroundServiceEventCallback != null) {
                 mForegroundServiceEventCallback.onSessionClosed(session);
@@ -799,6 +805,7 @@
 
         abstract static class ForegroundServiceEventCallback {
             public void onPlayerStateChanged(MediaSession session, @PlayerState int state) {}
+            public void onNotificationUpdateNeeded(MediaSession session) {}
             public void onSessionClosed(MediaSession session) {}
         }
     }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index d1db6dd..51caf49 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -1389,6 +1389,7 @@
                 item.addOnMetadataChangedListener(session.mCallbackExecutor, this);
             }
             mMediaItem = item;
+            session.getCallback().onCurrentMediaItemChanged(session.getInstance());
 
             boolean notifyingPended = false;
             if (item != null) {
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java b/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java
index cfed886..d8a459e 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java
@@ -222,7 +222,7 @@
      * notification UI.
      * <p>
      * This would be called on {@link MediaSession}'s callback executor when player state is
-     * changed.
+     * changed, or when the current media item of the session is changed.
      * <p>
      * With the notification returned here, the service becomes foreground service when the playback
      * is started. Apps targeting API {@link android.os.Build.VERSION_CODES#P} or later must request
diff --git a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java
index e0fb863..b9520b8 100644
--- a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java
+++ b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java
@@ -101,11 +101,10 @@
                 new SessionToken(mContext, MOCK_MEDIA2_SESSION_SERVICE), true, null);
 
         // Set current media item.
-        final String mediaId = "testMediaId";
         Bitmap albumArt = BitmapFactory.decodeResource(mContext.getResources(),
                 androidx.media2.test.service.R.drawable.big_buck_bunny);
         MediaMetadata metadata = new MediaMetadata.Builder()
-                .putText(METADATA_KEY_MEDIA_ID, mediaId)
+                .putText(METADATA_KEY_MEDIA_ID, "testMediaId")
                 .putText(METADATA_KEY_DISPLAY_TITLE, "Test Song Name")
                 .putText(METADATA_KEY_ARTIST, "Test Artist Name")
                 .putBitmap(METADATA_KEY_ALBUM_ART, albumArt)
@@ -122,4 +121,63 @@
         mPlayer.notifyPlayerStateChanged(SessionPlayer.PLAYER_STATE_PLAYING);
         Thread.sleep(NOTIFICATION_SHOW_TIME_MS);
     }
+
+    @Test
+    @Ignore("Comment out this line and manually run the test.")
+    public void notificationUpdatedWhenCurrentMediaItemChanged() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+            @Override
+            public SessionCommandGroup onConnect(@NonNull MediaSession session,
+                    @NonNull ControllerInfo controller) {
+                if (CLIENT_PACKAGE_NAME.equals(controller.getPackageName())) {
+                    mSession = session;
+                    // Change the player and playlist agent with ours.
+                    session.updatePlayer(mPlayer);
+                    latch.countDown();
+                }
+                return super.onConnect(session, controller);
+            }
+        };
+        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+
+        // Create a controller to start the service.
+        RemoteMediaController controller = createRemoteController(
+                new SessionToken(mContext, MOCK_MEDIA2_SESSION_SERVICE), true, null);
+
+        // Set current media item.
+        Bitmap albumArt = BitmapFactory.decodeResource(mContext.getResources(),
+                androidx.media2.test.service.R.drawable.big_buck_bunny);
+        MediaMetadata metadata = new MediaMetadata.Builder()
+                .putText(METADATA_KEY_MEDIA_ID, "testMediaId")
+                .putText(METADATA_KEY_DISPLAY_TITLE, "Test Song Name")
+                .putText(METADATA_KEY_ARTIST, "Test Artist Name")
+                .putBitmap(METADATA_KEY_ALBUM_ART, albumArt)
+                .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_NONE)
+                .putLong(METADATA_KEY_PLAYABLE, 1)
+                .build();
+        mPlayer.mCurrentMediaItem = new MediaItem.Builder()
+                .setMetadata(metadata)
+                .build();
+
+        mPlayer.notifyPlayerStateChanged(SessionPlayer.PLAYER_STATE_PLAYING);
+        // At this point, the notification should be shown.
+        Thread.sleep(NOTIFICATION_SHOW_TIME_MS);
+
+        // Set a new media item. (current media item is changed)
+        MediaMetadata newMetadata = new MediaMetadata.Builder()
+                .putText(METADATA_KEY_MEDIA_ID, "New media ID")
+                .putText(METADATA_KEY_DISPLAY_TITLE, "New Song Name")
+                .putText(METADATA_KEY_ARTIST, "New Artist Name")
+                .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_NONE)
+                .putLong(METADATA_KEY_PLAYABLE, 1)
+                .build();
+
+        MediaItem newItem = new MediaItem.Builder().setMetadata(newMetadata).build();
+        mPlayer.mCurrentMediaItem = newItem;
+
+        // Calling this should update the notification with the new metadata.
+        mPlayer.notifyCurrentMediaItemChanged(newItem);
+        Thread.sleep(NOTIFICATION_SHOW_TIME_MS);
+    }
 }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
index a65e7a2..2b98060 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -40,6 +40,7 @@
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -53,10 +54,9 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 
 /**
  * Provides non-system routes (and related RouteControllers) by using MediaRouter2.
@@ -154,9 +154,18 @@
 
     protected void refreshRoutes() {
         // Syetem routes should not be published by this provider.
-        List<MediaRoute2Info> newRoutes = mMediaRouter2.getRoutes().stream().distinct()
-                .filter(r -> !r.isSystemRoute())
-                .collect(Collectors.toList());
+        List<MediaRoute2Info> newRoutes = new ArrayList<>();
+        Set<MediaRoute2Info> route2InfoSet = new ArraySet<>();
+        for (MediaRoute2Info route : mMediaRouter2.getRoutes()) {
+            // A route should be unique
+            if (route == null || route2InfoSet.contains(route) || route.isSystemRoute()) {
+                continue;
+            }
+            route2InfoSet.add(route);
+
+            // Not using new ArrayList(route2InfoSet) here for preserving the order.
+            newRoutes.add(route);
+        }
 
         if (newRoutes.equals(mRoutes)) {
             return;
@@ -175,10 +184,13 @@
                     extras.getString(MediaRouter2Utils.KEY_ORIGINAL_ROUTE_ID));
         }
 
-        List<MediaRouteDescriptor> routeDescriptors = mRoutes.stream()
-                .map(MediaRouter2Utils::toMediaRouteDescriptor)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
+        List<MediaRouteDescriptor> routeDescriptors = new ArrayList<>();
+        for (MediaRoute2Info route : mRoutes) {
+            MediaRouteDescriptor descriptor = MediaRouter2Utils.toMediaRouteDescriptor(route);
+            if (route != null) {
+                routeDescriptors.add(descriptor);
+            }
+        }
         MediaRouteProviderDescriptor descriptor = new MediaRouteProviderDescriptor.Builder()
                 .setSupportsDynamicGroupRoute(true)
                 .addRoutes(routeDescriptors)
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
index a279629..cfde237 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
@@ -29,6 +29,7 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.Intent;
+import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderService;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
@@ -61,9 +62,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.UUID;
-import java.util.stream.Collectors;
 
 @RequiresApi(api = Build.VERSION_CODES.R)
 class MediaRoute2ProviderServiceAdapter extends MediaRoute2ProviderService {
@@ -279,12 +278,8 @@
 
     @Override
     public void onDiscoveryPreferenceChanged(@NonNull RouteDiscoveryPreference preference) {
-        MediaRouteSelector selector = new MediaRouteSelector.Builder()
-                .addControlCategories(preference.getPreferredFeatures().stream()
-                        .map(MediaRouter2Utils::toControlCategory)
-                        .collect(Collectors.toList())).build();
-        mServiceImpl.setBaseDiscoveryRequest(new MediaRouteDiscoveryRequest(selector,
-                preference.shouldPerformActiveScan()));
+        mServiceImpl.setBaseDiscoveryRequest(
+                MediaRouter2Utils.toMediaRouteDiscoveryRequest(preference));
     }
 
     public void setProviderDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
@@ -292,17 +287,26 @@
         List<MediaRouteDescriptor> routeDescriptors =
                 (descriptor == null) ? Collections.emptyList() : descriptor.getRoutes();
 
-        Map<String, MediaRouteDescriptor> descriptorMap =
-                routeDescriptors.stream().filter(Objects::nonNull)
-                        // Ignores duplicated route IDs.
-                        .collect(Collectors.toMap(r -> r.getId(), r -> r, (fst, snd) -> fst));
+        Map<String, MediaRouteDescriptor> descriptorMap = new ArrayMap<>();
+        for (MediaRouteDescriptor desc : routeDescriptors) {
+            // Ignores duplicated route IDs.
+            if (desc == null || descriptorMap.containsKey(desc.getId())) {
+                continue;
+            }
+            descriptorMap.put(desc.getId(), desc);
+        }
 
         updateStaticSessions(descriptorMap);
         // Handle duplicated IDs
-        notifyRoutes(descriptorMap.values().stream()
-                .map(MediaRouter2Utils::toFwkMediaRoute2Info)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList()));
+
+        List<MediaRoute2Info> routes = new ArrayList<>();
+        for (MediaRouteDescriptor desc : descriptorMap.values()) {
+            MediaRoute2Info fwkMediaRouteInfo = MediaRouter2Utils.toFwkMediaRoute2Info(desc);
+            if (fwkMediaRouteInfo != null) {
+                routes.add(fwkMediaRouteInfo);
+            }
+        }
+        notifyRoutes(routes);
     }
 
     private DynamicGroupRouteController findControllerBySessionId(String sessionId) {
@@ -354,11 +358,13 @@
     }
 
     void updateStaticSessions(Map<String, MediaRouteDescriptor> routeDescriptors) {
-        List<SessionRecord> staticSessions;
+        List<SessionRecord> staticSessions = new ArrayList<>();
         synchronized (mLock) {
-            staticSessions = mSessionRecords.values().stream()
-                    .filter(r -> (r.getFlags() & SessionRecord.SESSION_FLAG_DYNAMIC) == 0)
-                    .collect(Collectors.toList());
+            for (SessionRecord sessionRecord : mSessionRecords.values()) {
+                if ((sessionRecord.getFlags() & SessionRecord.SESSION_FLAG_DYNAMIC) == 0) {
+                    staticSessions.add(sessionRecord);
+                }
+            }
         }
         for (SessionRecord sessionRecord : staticSessions) {
             DynamicGroupRouteControllerProxy controller =
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
index ea442e4..91db8ba 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
@@ -33,6 +33,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -42,9 +43,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 //TODO: Remove SuppressLInt
 @SuppressLint("NewApi")
@@ -185,14 +184,26 @@
         if (features == null) {
             return new ArrayList<>();
         }
-        return features.stream().distinct().map(f -> {
+
+        List<IntentFilter> controlFilters = new ArrayList<>();
+        Set<String> featuresSet = new ArraySet<>();
+        for (String feature : features) {
+            // A feature should be unique.
+            if (featuresSet.contains(feature)) {
+                continue;
+            }
+            featuresSet.add(feature);
+
             IntentFilter filter = new IntentFilter();
-            filter.addCategory(toControlCategory(f));
+            filter.addCategory(toControlCategory(feature));
             // TODO: Add actions by using extras. (see RemotePlaybackClient#detectFeatures())
             // filter.addAction(MediaControlIntent.ACTION_PLAY);
             // filter.addAction(MediaControlIntent.ACTION_SEEK);
-            return filter;
-        }).collect(Collectors.toList());
+
+            controlFilters.add(filter);
+        }
+
+        return controlFilters;
     }
 
     @NonNull
@@ -200,8 +211,29 @@
         if (routes == null) {
             return new ArrayList<>();
         }
-        return routes.stream().filter(Objects::nonNull)
-                .map(MediaRoute2Info::getId).collect(Collectors.toList());
+
+        List<String> routeIds = new ArrayList<>();
+        for (MediaRoute2Info route : routes) {
+            if (route == null) {
+                continue;
+            }
+            routeIds.add(route.getId());
+        }
+        return routeIds;
+    }
+
+    @NonNull
+    static MediaRouteDiscoveryRequest toMediaRouteDiscoveryRequest(
+            @NonNull RouteDiscoveryPreference preference) {
+        List<String> controlCategories = new ArrayList<>();
+        for (String feature : preference.getPreferredFeatures()) {
+            controlCategories.add(MediaRouter2Utils.toControlCategory(feature));
+        }
+        MediaRouteSelector selector = new MediaRouteSelector.Builder()
+                .addControlCategories(controlCategories)
+                .build();
+
+        return new MediaRouteDiscoveryRequest(selector, preference.shouldPerformActiveScan());
     }
 
     @NonNull
@@ -211,9 +243,11 @@
             return new RouteDiscoveryPreference.Builder(new ArrayList<>(), false).build();
         }
         boolean activeScan = discoveryRequest.isActiveScan();
-        List<String> routeFeatures = discoveryRequest.getSelector().getControlCategories()
-                .stream().map(MediaRouter2Utils::toRouteFeature)
-                .collect(Collectors.toList());
+
+        List<String> routeFeatures = new ArrayList<>();
+        for (String controlCategory : discoveryRequest.getSelector().getControlCategories()) {
+            routeFeatures.add(MediaRouter2Utils.toRouteFeature(controlCategory));
+        }
         return new RouteDiscoveryPreference.Builder(routeFeatures, activeScan).build();
     }
 
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
index 3126485..8141dc1 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
@@ -34,7 +34,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * Watches for media route provider services to be installed.
@@ -163,8 +162,12 @@
     @NonNull
     List<ServiceInfo> getMediaRoute2ProviderServices() {
         Intent intent = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
-        return mPackageManager.queryIntentServices(intent, 0).stream()
-                .map(resolveInfo -> resolveInfo.serviceInfo).collect(Collectors.toList());
+
+        List<ServiceInfo> serviceInfoList = new ArrayList<>();
+        for (ResolveInfo resolveInfo : mPackageManager.queryIntentServices(intent, 0)) {
+            serviceInfoList.add(resolveInfo.serviceInfo);
+        }
+        return serviceInfoList;
     }
 
     private int findProvider(String packageName, String className) {
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
index 6a64d84..e0469ed 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
@@ -23,7 +23,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Divider
 import androidx.compose.material.Text
 import androidx.compose.material.TextField
@@ -74,7 +74,7 @@
         Divider(color = Color.Black)
         Button(
             onClick = { navController.navigate(Uri.parse(uri + state.value)) },
-            colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+            colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
             modifier = Modifier.fillMaxWidth()
         ) {
             Text(text = "Navigate By DeepLink")
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
index fd00b02..a195480a 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
@@ -21,7 +21,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -53,7 +53,7 @@
         if (number < 5) {
             Button(
                 onClick = { navController.navigate("$next") },
-                colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+                colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
                 modifier = Modifier.fillMaxWidth()
             ) {
                 Text(text = "Navigate to Screen $next")
@@ -63,7 +63,7 @@
         if (navController.previousBackStackEntry != null) {
             Button(
                 onClick = { navController.navigate("1") { popUpTo("1") { inclusive = true } } },
-                colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+                colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
                 modifier = Modifier.fillMaxWidth()
             ) {
                 Text(text = "PopUpTo Screen 1")
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
index 4da4784..89c6817 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
@@ -25,7 +25,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Divider
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -133,7 +133,7 @@
 ) {
     Button(
         onClick = listener,
-        colors = ButtonConstants.defaultButtonColors(backgroundColor = LightGray),
+        colors = ButtonDefaults.buttonColors(backgroundColor = LightGray),
         modifier = Modifier.fillMaxWidth()
     ) {
         Text(text = "Navigate to $text")
@@ -145,7 +145,7 @@
     if (navController.previousBackStackEntry != null) {
         Button(
             onClick = { navController.popBackStack() },
-            colors = ButtonConstants.defaultButtonColors(backgroundColor = LightGray),
+            colors = ButtonDefaults.buttonColors(backgroundColor = LightGray),
             modifier = Modifier.fillMaxWidth()
         ) {
             Text(text = "Go to Previous screen")
diff --git a/paging/common/api/current.txt b/paging/common/api/current.txt
index dfb2f53..c0867a3 100644
--- a/paging/common/api/current.txt
+++ b/paging/common/api/current.txt
@@ -9,10 +9,7 @@
   }
 
   public final class CombinedLoadStates {
-    ctor public CombinedLoadStates(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
-    method public androidx.paging.LoadStates component1();
-    method public androidx.paging.LoadStates? component2();
-    method public androidx.paging.CombinedLoadStates copy(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
+    ctor public CombinedLoadStates(androidx.paging.LoadState refresh, androidx.paging.LoadState prepend, androidx.paging.LoadState append, androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
     method public androidx.paging.LoadState getAppend();
     method public androidx.paging.LoadStates? getMediator();
     method public androidx.paging.LoadState getPrepend();
@@ -265,7 +262,7 @@
   }
 
   public final class Pager<Key, Value> {
-    ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
+    ctor @androidx.paging.ExperimentalPagingApi public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     ctor public Pager(androidx.paging.PagingConfig config, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>> getFlow();
@@ -318,8 +315,8 @@
     method public final boolean getInvalid();
     method public boolean getJumpingSupported();
     method public boolean getKeyReuseSupported();
-    method @androidx.paging.ExperimentalPagingApi public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
-    method public void invalidate();
+    method public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
+    method public final void invalidate();
     method public abstract suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>> p);
     method public final void registerInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
     method public final void unregisterInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
@@ -331,30 +328,25 @@
   public abstract static sealed class PagingSource.LoadParams<Key> {
     method public abstract Key? getKey();
     method public final int getLoadSize();
-    method @Deprecated public final int getPageSize();
     method public final boolean getPlaceholdersEnabled();
     property public abstract Key? key;
     property public final int loadSize;
-    property @Deprecated public final int pageSize;
     property public final boolean placeholdersEnabled;
   }
 
   public static final class PagingSource.LoadParams.Append<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Append(Key key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Append(Key key, int loadSize, boolean placeholdersEnabled);
     method public Key getKey();
     property public Key key;
   }
 
   public static final class PagingSource.LoadParams.Prepend<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Prepend(Key key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Prepend(Key key, int loadSize, boolean placeholdersEnabled);
     method public Key getKey();
     property public Key key;
   }
 
   public static final class PagingSource.LoadParams.Refresh<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Refresh(Key? key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Refresh(Key? key, int loadSize, boolean placeholdersEnabled);
     method public Key? getKey();
     property public Key? key;
diff --git a/paging/common/api/public_plus_experimental_current.txt b/paging/common/api/public_plus_experimental_current.txt
index c2059933..9641951 100644
--- a/paging/common/api/public_plus_experimental_current.txt
+++ b/paging/common/api/public_plus_experimental_current.txt
@@ -9,11 +9,7 @@
   }
 
   public final class CombinedLoadStates {
-    ctor public CombinedLoadStates(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
-    method public androidx.paging.LoadStates component1();
-    method public androidx.paging.LoadStates? component2();
-    method public androidx.paging.CombinedLoadStates copy(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public inline void forEach(kotlin.jvm.functions.Function3<? super androidx.paging.LoadType,? super java.lang.Boolean,? super androidx.paging.LoadState,kotlin.Unit> op);
+    ctor public CombinedLoadStates(androidx.paging.LoadState refresh, androidx.paging.LoadState prepend, androidx.paging.LoadState append, androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
     method public androidx.paging.LoadState getAppend();
     method public androidx.paging.LoadStates? getMediator();
     method public androidx.paging.LoadState getPrepend();
@@ -267,7 +263,7 @@
   }
 
   public final class Pager<Key, Value> {
-    ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
+    ctor @androidx.paging.ExperimentalPagingApi public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     ctor public Pager(androidx.paging.PagingConfig config, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>> getFlow();
@@ -320,8 +316,8 @@
     method public final boolean getInvalid();
     method public boolean getJumpingSupported();
     method public boolean getKeyReuseSupported();
-    method @androidx.paging.ExperimentalPagingApi public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
-    method public void invalidate();
+    method public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
+    method public final void invalidate();
     method public abstract suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>> p);
     method public final void registerInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
     method public final void unregisterInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
@@ -333,30 +329,25 @@
   public abstract static sealed class PagingSource.LoadParams<Key> {
     method public abstract Key? getKey();
     method public final int getLoadSize();
-    method @Deprecated public final int getPageSize();
     method public final boolean getPlaceholdersEnabled();
     property public abstract Key? key;
     property public final int loadSize;
-    property @Deprecated public final int pageSize;
     property public final boolean placeholdersEnabled;
   }
 
   public static final class PagingSource.LoadParams.Append<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Append(Key key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Append(Key key, int loadSize, boolean placeholdersEnabled);
     method public Key getKey();
     property public Key key;
   }
 
   public static final class PagingSource.LoadParams.Prepend<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Prepend(Key key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Prepend(Key key, int loadSize, boolean placeholdersEnabled);
     method public Key getKey();
     property public Key key;
   }
 
   public static final class PagingSource.LoadParams.Refresh<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Refresh(Key? key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Refresh(Key? key, int loadSize, boolean placeholdersEnabled);
     method public Key? getKey();
     property public Key? key;
diff --git a/paging/common/api/restricted_current.txt b/paging/common/api/restricted_current.txt
index dfb2f53..c0867a3 100644
--- a/paging/common/api/restricted_current.txt
+++ b/paging/common/api/restricted_current.txt
@@ -9,10 +9,7 @@
   }
 
   public final class CombinedLoadStates {
-    ctor public CombinedLoadStates(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
-    method public androidx.paging.LoadStates component1();
-    method public androidx.paging.LoadStates? component2();
-    method public androidx.paging.CombinedLoadStates copy(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
+    ctor public CombinedLoadStates(androidx.paging.LoadState refresh, androidx.paging.LoadState prepend, androidx.paging.LoadState append, androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
     method public androidx.paging.LoadState getAppend();
     method public androidx.paging.LoadStates? getMediator();
     method public androidx.paging.LoadState getPrepend();
@@ -265,7 +262,7 @@
   }
 
   public final class Pager<Key, Value> {
-    ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
+    ctor @androidx.paging.ExperimentalPagingApi public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     ctor public Pager(androidx.paging.PagingConfig config, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>> getFlow();
@@ -318,8 +315,8 @@
     method public final boolean getInvalid();
     method public boolean getJumpingSupported();
     method public boolean getKeyReuseSupported();
-    method @androidx.paging.ExperimentalPagingApi public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
-    method public void invalidate();
+    method public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
+    method public final void invalidate();
     method public abstract suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>> p);
     method public final void registerInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
     method public final void unregisterInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
@@ -331,30 +328,25 @@
   public abstract static sealed class PagingSource.LoadParams<Key> {
     method public abstract Key? getKey();
     method public final int getLoadSize();
-    method @Deprecated public final int getPageSize();
     method public final boolean getPlaceholdersEnabled();
     property public abstract Key? key;
     property public final int loadSize;
-    property @Deprecated public final int pageSize;
     property public final boolean placeholdersEnabled;
   }
 
   public static final class PagingSource.LoadParams.Append<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Append(Key key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Append(Key key, int loadSize, boolean placeholdersEnabled);
     method public Key getKey();
     property public Key key;
   }
 
   public static final class PagingSource.LoadParams.Prepend<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Prepend(Key key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Prepend(Key key, int loadSize, boolean placeholdersEnabled);
     method public Key getKey();
     property public Key key;
   }
 
   public static final class PagingSource.LoadParams.Refresh<Key> extends androidx.paging.PagingSource.LoadParams<Key> {
-    ctor public PagingSource.LoadParams.Refresh(Key? key, int loadSize, boolean placeholdersEnabled, int pageSize);
     ctor public PagingSource.LoadParams.Refresh(Key? key, int loadSize, boolean placeholdersEnabled);
     method public Key? getKey();
     property public Key? key;
diff --git a/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt b/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt
index 2b4bee4..de3a633 100644
--- a/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt
@@ -42,7 +42,6 @@
  * An intermediate flow producer that flattens previous page events and gives any new downstream
  * just those events instead of the full history.
  */
-@OptIn(ExperimentalCoroutinesApi::class)
 internal class CachedPageEventFlow<T : Any>(
     src: Flow<PageEvent<T>>,
     scope: CoroutineScope
@@ -81,6 +80,7 @@
         multicastedSrc.close()
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     val downstreamFlow = channelFlow {
         // get a new snapshot. this will immediately hook us to the upstream channel
         val snapshot = pageController.createTemporaryDownstream()
@@ -141,7 +141,6 @@
      */
     private val historyChannel: Channel<IndexedValue<PageEvent<T>>> = Channel(Channel.UNLIMITED)
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     fun consumeHistory() = historyChannel.consumeAsFlow()
 
     /**
diff --git a/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt b/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt
index 828d8a0..630c588 100644
--- a/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt
@@ -30,7 +30,6 @@
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.scan
 
-@OptIn(ExperimentalCoroutinesApi::class)
 private class MulticastedPagingData<T : Any>(
     val scope: CoroutineScope,
     val parent: PagingData<T>,
diff --git a/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt b/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt
index 386b4ba..19efffd 100644
--- a/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt
@@ -16,12 +16,43 @@
 
 package androidx.paging
 
-import androidx.annotation.RestrictTo
-
 /**
  * Collection of pagination [LoadState]s for both a [PagingSource], and [RemoteMediator].
  */
-data class CombinedLoadStates(
+class CombinedLoadStates(
+    /**
+     * Convenience for combined behavior of [REFRESH][LoadType.REFRESH] [LoadState], which
+     * generally defers to [mediator] if it exists, but if previously was [LoadState.Loading],
+     * awaits for both [source] and [mediator] to become [LoadState.NotLoading] to ensure the
+     * remote load was applied.
+     *
+     * For use cases that require reacting to [LoadState] of [source] and [mediator]
+     * specifically, e.g., showing cached data when network loads via [mediator] fail,
+     * [LoadStates] exposed via [source] and [mediator] should be used directly.
+     */
+    val refresh: LoadState,
+    /**
+     * Convenience for combined behavior of [PREPEND][LoadType.REFRESH] [LoadState], which
+     * generally defers to [mediator] if it exists, but if previously was [LoadState.Loading],
+     * awaits for both [source] and [mediator] to become [LoadState.NotLoading] to ensure the
+     * remote load was applied.
+     *
+     * For use cases that require reacting to [LoadState] of [source] and [mediator]
+     * specifically, e.g., showing cached data when network loads via [mediator] fail,
+     * [LoadStates] exposed via [source] and [mediator] should be used directly.
+     */
+    val prepend: LoadState,
+    /**
+     * Convenience for combined behavior of [APPEND][LoadType.REFRESH] [LoadState], which
+     * generally defers to [mediator] if it exists, but if previously was [LoadState.Loading],
+     * awaits for both [source] and [mediator] to become [LoadState.NotLoading] to ensure the
+     * remote load was applied.
+     *
+     * For use cases that require reacting to [LoadState] of [source] and [mediator]
+     * specifically, e.g., showing cached data when network loads via [mediator] fail,
+     * [LoadStates] exposed via [source] and [mediator] should be used directly.
+     */
+    val append: LoadState,
     /**
      * [LoadStates] corresponding to loads from a [PagingSource].
      */
@@ -31,40 +62,39 @@
      * [LoadStates] corresponding to loads from a [RemoteMediator], or `null` if [RemoteMediator]
      * not present.
      */
-    val mediator: LoadStates? = null
+    val mediator: LoadStates? = null,
 ) {
-    /**
-     * Convenience for accessing [REFRESH][LoadType.REFRESH] [LoadState], which always defers to
-     * [LoadState] of [mediator] if it exists, otherwise equivalent to [LoadState] of [source].
-     *
-     * For use cases that require reacting to [LoadState] of [source] and [mediator]
-     * specifically, e.g., showing cached data when network loads via [mediator] fail,
-     * [LoadStates] exposed via [source] and [mediator] should be used directly.
-     */
-    val refresh: LoadState = (mediator ?: source).refresh
 
-    /**
-     * Convenience for accessing [PREPEND][LoadType.PREPEND] [LoadState], which always defers to
-     * [LoadState] of [mediator] if it exists, otherwise equivalent to [LoadState] of [source].
-     *
-     * For use cases that require reacting to [LoadState] of [source] and [mediator]
-     * specifically, e.g., showing cached data when network loads via [mediator] fail,
-     * [LoadStates] exposed via [source] and [mediator] should be used directly.
-     */
-    val prepend: LoadState = (mediator ?: source).prepend
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
 
-    /**
-     * Convenience for accessing [APPEND][LoadType.APPEND] [LoadState], which always defers to
-     * [LoadState] of [mediator] if it exists, otherwise equivalent to [LoadState] of [source].
-     *
-     * For use cases that require reacting to [LoadState] of [source] and [mediator]
-     * specifically, e.g., showing cached data when network loads via [mediator] fail,
-     * [LoadStates] exposed via [source] and [mediator] should be used directly.
-     */
-    val append: LoadState = (mediator ?: source).append
+        other as CombinedLoadStates
 
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    inline fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
+        if (refresh != other.refresh) return false
+        if (prepend != other.prepend) return false
+        if (append != other.append) return false
+        if (source != other.source) return false
+        if (mediator != other.mediator) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = refresh.hashCode()
+        result = 31 * result + prepend.hashCode()
+        result = 31 * result + append.hashCode()
+        result = 31 * result + source.hashCode()
+        result = 31 * result + (mediator?.hashCode() ?: 0)
+        return result
+    }
+
+    override fun toString(): String {
+        return "CombinedLoadStates(refresh=$refresh, prepend=$prepend, append=$append, " +
+            "source=$source, mediator=$mediator)"
+    }
+
+    internal fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
         source.forEach { type, state ->
             op(type, false, state)
         }
@@ -75,11 +105,10 @@
 
     internal companion object {
         val IDLE_SOURCE = CombinedLoadStates(
+            refresh = LoadState.NotLoading.Incomplete,
+            prepend = LoadState.NotLoading.Incomplete,
+            append = LoadState.NotLoading.Incomplete,
             source = LoadStates.IDLE
         )
-        val IDLE_MEDIATOR = CombinedLoadStates(
-            source = LoadStates.IDLE,
-            mediator = LoadStates.IDLE
-        )
     }
 }
diff --git a/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt b/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt
index d896993..3679a19 100644
--- a/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt
@@ -16,7 +16,6 @@
 
 package androidx.paging
 
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.mapNotNull
 
@@ -25,7 +24,6 @@
  * * It allows not setting an initial value
  * * Sending duplicate values is allowed
  */
-@OptIn(ExperimentalCoroutinesApi::class)
 internal class ConflatedEventBus<T : Any>(initialValue: T? = null) {
     private val state = MutableStateFlow(Pair(Integer.MIN_VALUE, initialValue))
 
diff --git a/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt b/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt
index 055f941..e7d9382 100644
--- a/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt
@@ -97,7 +97,6 @@
     @Suppress("UNCHECKED_CAST")
     override val lastKey: K?
         get() {
-            @OptIn(ExperimentalPagingApi::class)
             return (storage.getRefreshKeyInfo(config) as PagingState<K, V>?)
                 ?.let { pagingSource.getRefreshKey(it) }
                 ?: initialLastKey
diff --git a/paging/common/src/main/kotlin/androidx/paging/DataSource.kt b/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
index 26e0270..cbceeae 100644
--- a/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
@@ -233,9 +233,12 @@
         @JvmOverloads
         fun asPagingSourceFactory(
             fetchDispatcher: CoroutineDispatcher = Dispatchers.IO
-        ): () -> PagingSource<Key, Value> = {
-            LegacyPagingSource(fetchDispatcher) { create() }
-        }
+        ): () -> PagingSource<Key, Value> = SuspendingPagingSourceFactory(
+            delegate = {
+                LegacyPagingSource(fetchDispatcher, create())
+            },
+            dispatcher = fetchDispatcher
+        )
     }
 
     /**
diff --git a/paging/common/src/main/kotlin/androidx/paging/DirectDispatcher.kt b/paging/common/src/main/kotlin/androidx/paging/DirectDispatcher.kt
deleted file mode 100644
index 5d65446..0000000
--- a/paging/common/src/main/kotlin/androidx/paging/DirectDispatcher.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.paging
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlin.coroutines.CoroutineContext
-
-/**
- * [CoroutineDispatcher] which immediately runs new jobs on the current thread.
- */
-internal object DirectDispatcher : CoroutineDispatcher() {
-    override fun dispatch(context: CoroutineContext, block: Runnable) {
-        block.run()
-    }
-}
\ No newline at end of file
diff --git a/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt b/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt
index 5d24516..7014826 100644
--- a/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt
@@ -17,6 +17,7 @@
 package androidx.paging
 
 import androidx.annotation.RestrictTo
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 
 /**
@@ -31,15 +32,17 @@
 class InitialPagedList<K : Any, V : Any>(
     pagingSource: PagingSource<K, V>,
     coroutineScope: CoroutineScope,
+    notifyDispatcher: CoroutineDispatcher,
+    backgroundDispatcher: CoroutineDispatcher,
     config: Config,
     initialLastKey: K?
 ) : ContiguousPagedList<K, V>(
-    pagingSource,
-    coroutineScope,
-    DirectDispatcher,
-    DirectDispatcher,
-    null,
-    config,
-    PagingSource.LoadResult.Page.empty(),
-    initialLastKey
+    pagingSource = pagingSource,
+    coroutineScope = coroutineScope,
+    notifyDispatcher = notifyDispatcher,
+    backgroundDispatcher = backgroundDispatcher,
+    boundaryCallback = null,
+    config = config,
+    initialPage = PagingSource.LoadResult.Page.empty(),
+    initialLastKey = initialLastKey
 )
diff --git a/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt b/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt
index c053edd..1bc1f44 100644
--- a/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt
@@ -119,7 +119,6 @@
             key,
             config.pageSize,
             config.enablePlaceholders,
-            config.pageSize
         )
         scheduleLoad(LoadType.PREPEND, loadParams)
     }
@@ -136,7 +135,6 @@
             key,
             config.pageSize,
             config.enablePlaceholders,
-            config.pageSize
         )
         scheduleLoad(LoadType.APPEND, loadParams)
     }
diff --git a/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt b/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt
index d3b44a6..c5d7845 100644
--- a/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt
@@ -30,24 +30,46 @@
  * A wrapper around [DataSource] which adapts it to the [PagingSource] API.
  */
 internal class LegacyPagingSource<Key : Any, Value : Any>(
-    private val fetchDispatcher: CoroutineDispatcher = DirectDispatcher,
-    internal val dataSourceFactory: () -> DataSource<Key, Value>
+    private val fetchDispatcher: CoroutineDispatcher,
+    internal val dataSource: DataSource<Key, Value>
 ) : PagingSource<Key, Value>() {
-    // Lazily initialize because it must be created on fetchDispatcher, but PagingSourceFactory
-    // passed to Pager is a non-suspending method.
-    internal val dataSource by lazy {
-        dataSourceFactory().also { dataSource ->
-            dataSource.addInvalidatedCallback(::invalidate)
-            // LegacyPagingSource registers invalidate callback after DataSource is created, so we
-            // need to check for race condition here. If DataSource is already invalid, simply
-            // propagate invalidation manually.
-            if (dataSource.isInvalid && !invalid) {
-                dataSource.removeInvalidatedCallback(::invalidate)
-                // Note: Calling this.invalidate directly will re-evaluate dataSource's by lazy
-                // init block, since we haven't returned a value for dataSource yet.
-                super.invalidate()
+    private var pageSize: Int = PAGE_SIZE_NOT_SET
+
+    init {
+        dataSource.addInvalidatedCallback(::invalidate)
+        // technically, there is a possibly race where data source might call back our invalidate.
+        // in practice, it is fine because all fields are initialized at this point.
+        registerInvalidatedCallback {
+            dataSource.removeInvalidatedCallback(::invalidate)
+            dataSource.invalidate()
+        }
+
+        // LegacyPagingSource registers invalidate callback after DataSource is created, so we
+        // need to check for race condition here. If DataSource is already invalid, simply
+        // propagate invalidation manually.
+        if (!invalid && dataSource.isInvalid) {
+            invalidate()
+        }
+    }
+
+    fun setPageSize(pageSize: Int) {
+        check(this.pageSize == PAGE_SIZE_NOT_SET || pageSize == this.pageSize) {
+            "Page size is already set to ${this.pageSize}."
+        }
+        this.pageSize = pageSize
+    }
+
+    /**
+     * This only ever happens in testing if Pager / PagedList is not used hence we'll not get the
+     * page size. For those cases, guess :).
+     */
+    private fun guessPageSize(params: LoadParams<Key>): Int {
+        if (params is LoadParams.Refresh) {
+            if (params.loadSize % PagingConfig.DEFAULT_INITIAL_PAGE_MULTIPLIER == 0) {
+                return params.loadSize / PagingConfig.DEFAULT_INITIAL_PAGE_MULTIPLIER
             }
         }
+        return params.loadSize
     }
 
     override suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value> {
@@ -56,13 +78,30 @@
             is LoadParams.Append -> APPEND
             is LoadParams.Prepend -> PREPEND
         }
+        if (pageSize == PAGE_SIZE_NOT_SET) {
+            // println because we don't have android logger here
+            println(
+                """
+                WARNING: pageSize on the LegacyPagingSource is not set.
+                When using legacy DataSource / DataSourceFactory with Paging3, page size
+                should've been set by the paging library but it is not set yet.
+
+                If you are seeing this message in tests where you are testing DataSource
+                in isolation (without a Pager), it is expected and page size will be estimated
+                based on parameters.
+
+                If you are seeing this message despite using a Pager, please file a bug:
+                https://issuetracker.google.com/issues/new?component=413106
+                """.trimIndent()
+            )
+            pageSize = guessPageSize(params)
+        }
         val dataSourceParams = Params(
             type,
             params.key,
             params.loadSize,
             params.placeholdersEnabled,
-            @Suppress("DEPRECATION")
-            params.pageSize
+            pageSize
         )
 
         return withContext(fetchDispatcher) {
@@ -80,12 +119,6 @@
         }
     }
 
-    override fun invalidate() {
-        super.invalidate()
-        dataSource.invalidate()
-    }
-
-    @OptIn(ExperimentalPagingApi::class)
     @Suppress("UNCHECKED_CAST")
     override fun getRefreshKey(state: PagingState<Key, Value>): Key? {
         return when (dataSource.type) {
@@ -105,4 +138,8 @@
 
     override val jumpingSupported: Boolean
         get() = dataSource.type == POSITIONAL
+
+    companion object {
+        const val PAGE_SIZE_NOT_SET = Integer.MIN_VALUE
+    }
 }
diff --git a/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt b/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt
index baaf577..31ed7eb 100644
--- a/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt
@@ -16,27 +16,45 @@
 
 package androidx.paging
 
-import androidx.annotation.RestrictTo
+import androidx.paging.LoadState.Error
+import androidx.paging.LoadState.Loading
+import androidx.paging.LoadState.NotLoading
 
 /**
- * TODO: Remove this once [PageEvent.LoadStateUpdate] contained [CombinedLoadStates].
- *
- * @hide
+ * Helper to construct [CombinedLoadStates] that accounts for previous state to set the convenience
+ * properties correctly.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class MutableLoadStateCollection {
+internal class MutableLoadStateCollection {
+    private var refresh: LoadState = NotLoading.Incomplete
+    private var prepend: LoadState = NotLoading.Incomplete
+    private var append: LoadState = NotLoading.Incomplete
     private var source: LoadStates = LoadStates.IDLE
     private var mediator: LoadStates? = null
 
-    fun snapshot() = CombinedLoadStates(source, mediator)
+    fun snapshot() = CombinedLoadStates(
+        refresh = refresh,
+        prepend = prepend,
+        append = append,
+        source = source,
+        mediator = mediator,
+    )
 
     fun set(combinedLoadStates: CombinedLoadStates) {
+        refresh = combinedLoadStates.refresh
+        prepend = combinedLoadStates.prepend
+        append = combinedLoadStates.append
         source = combinedLoadStates.source
         mediator = combinedLoadStates.mediator
     }
 
+    fun set(sourceLoadStates: LoadStates, remoteLoadStates: LoadStates?) {
+        source = sourceLoadStates
+        mediator = remoteLoadStates
+        updateHelperStates()
+    }
+
     fun set(type: LoadType, remote: Boolean, state: LoadState): Boolean {
-        return if (remote) {
+        val didChange = if (remote) {
             val lastMediator = mediator
             mediator = (mediator ?: LoadStates.IDLE).modifyState(type, state)
             mediator != lastMediator
@@ -45,12 +63,61 @@
             source = source.modifyState(type, state)
             source != lastSource
         }
+
+        updateHelperStates()
+        return didChange
     }
 
     fun get(type: LoadType, remote: Boolean): LoadState? {
         return (if (remote) mediator else source)?.get(type)
     }
 
+    private fun updateHelperStates() {
+        refresh = computeHelperState(
+            previousState = refresh,
+            sourceRefreshState = source.refresh,
+            sourceState = source.refresh,
+            remoteState = mediator?.refresh
+        )
+        prepend = computeHelperState(
+            previousState = prepend,
+            sourceRefreshState = source.refresh,
+            sourceState = source.prepend,
+            remoteState = mediator?.prepend
+        )
+        append = computeHelperState(
+            previousState = append,
+            sourceRefreshState = source.refresh,
+            sourceState = source.append,
+            remoteState = mediator?.append
+        )
+    }
+
+    /**
+     * Computes the next value for the convenience helpers in [CombinedLoadStates], which
+     * generally defers to remote state, but waits for both source and remote states to become
+     * [NotLoading] before moving to that state. This provides a reasonable default for the common
+     * use-case where you generally want to wait for both RemoteMediator to return and for the
+     * update to get applied before signaling to UI that a network fetch has "finished".
+     */
+    private fun computeHelperState(
+        previousState: LoadState,
+        sourceRefreshState: LoadState,
+        sourceState: LoadState,
+        remoteState: LoadState?
+    ): LoadState {
+        if (remoteState == null) return sourceState
+
+        return when (previousState) {
+            is Loading -> when {
+                sourceRefreshState is NotLoading && remoteState is NotLoading -> remoteState
+                remoteState is Error -> remoteState
+                else -> previousState
+            }
+            else -> remoteState
+        }
+    }
+
     internal inline fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
         source.forEach { type, state ->
             op(type, false, state)
@@ -59,4 +126,9 @@
             op(type, true, state)
         }
     }
+
+    internal fun terminates(loadType: LoadType): Boolean {
+        return get(loadType, false)!!.endOfPaginationReached &&
+            get(loadType, true)?.endOfPaginationReached != false
+    }
 }
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt b/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
index 827e631..f44eb824 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
@@ -39,11 +39,11 @@
     ) : PageEvent<T>() {
         init {
             require(loadType == APPEND || placeholdersBefore >= 0) {
-                "Append state defining placeholdersBefore must be > 0, but was" +
+                "Prepend insert defining placeholdersBefore must be > 0, but was" +
                     " $placeholdersBefore"
             }
             require(loadType == PREPEND || placeholdersAfter >= 0) {
-                "Prepend state defining placeholdersAfter must be > 0, but was" +
+                "Append insert defining placeholdersAfter must be > 0, but was" +
                     " $placeholdersAfter"
             }
             require(loadType != REFRESH || pages.isNotEmpty()) {
@@ -150,11 +150,14 @@
                 placeholdersBefore = 0,
                 placeholdersAfter = 0,
                 combinedLoadStates = CombinedLoadStates(
+                    refresh = LoadState.NotLoading.Incomplete,
+                    prepend = LoadState.NotLoading.Complete,
+                    append = LoadState.NotLoading.Complete,
                     source = LoadStates(
                         refresh = LoadState.NotLoading.Incomplete,
                         prepend = LoadState.NotLoading.Complete,
-                        append = LoadState.NotLoading.Complete
-                    )
+                        append = LoadState.NotLoading.Complete,
+                    ),
                 )
             )
         }
@@ -212,8 +215,7 @@
              * This prevents multiple related RV animations from happening simultaneously
              */
             internal fun canDispatchWithoutInsert(loadState: LoadState, fromMediator: Boolean) =
-                loadState is LoadState.Loading || loadState is LoadState.Error ||
-                    (loadState.endOfPaginationReached && fromMediator)
+                loadState is LoadState.Loading || loadState is LoadState.Error || fromMediator
         }
     }
 
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
index eccc298..06d6d7a 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
@@ -31,9 +31,9 @@
 import kotlinx.coroutines.flow.scan
 import kotlinx.coroutines.launch
 
-@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+@OptIn(FlowPreview::class)
 internal class PageFetcher<Key : Any, Value : Any>(
-    private val pagingSourceFactory: () -> PagingSource<Key, Value>,
+    private val pagingSourceFactory: suspend () -> PagingSource<Key, Value>,
     private val initialKey: Key?,
     private val config: PagingConfig,
     @OptIn(ExperimentalPagingApi::class)
@@ -53,6 +53,7 @@
 
     // The object built by paging builder can maintain the scope so that on rotation we don't stop
     // the paging.
+    @OptIn(ExperimentalCoroutinesApi::class)
     val flow: Flow<PagingData<Value>> = channelFlow {
         val remoteMediatorAccessor = remoteMediator?.let {
             RemoteMediatorAccessor(this, it)
@@ -87,7 +88,6 @@
                     previousPagingState = previousGeneration.state
                 }
 
-                @OptIn(ExperimentalPagingApi::class)
                 val initialKey: Key? = previousPagingState?.let { pagingSource.getRefreshKey(it) }
                     ?: initialKey
 
@@ -110,11 +110,10 @@
             }
             .filterNotNull()
             .mapLatest { generation ->
-                val downstreamFlow = if (remoteMediatorAccessor == null) {
-                    generation.snapshot.pageEventFlow
-                } else {
-                    generation.snapshot.injectRemoteEvents(remoteMediatorAccessor)
-                }
+                val downstreamFlow = generation.snapshot
+                    .injectRemoteEvents(remoteMediatorAccessor)
+                // .mapRemoteCompleteAsTrailingInsertForSeparators()
+
                 PagingData(
                     flow = downstreamFlow,
                     receiver = PagerUiReceiver(generation.snapshot, retryEvents)
@@ -124,47 +123,79 @@
     }
 
     private fun PageFetcherSnapshot<Key, Value>.injectRemoteEvents(
-        accessor: RemoteMediatorAccessor<Key, Value>
-    ): Flow<PageEvent<Value>> = channelFlow {
-        suspend fun dispatchIfValid(type: LoadType, state: LoadState) {
-            // not loading events are sent w/ insert-drop events.
-            if (PageEvent.LoadStateUpdate.canDispatchWithoutInsert(state, fromMediator = true)) {
-                send(
-                    PageEvent.LoadStateUpdate<Value>(type, true, state)
-                )
-            } else {
-                // ignore. Some invalidation will happened and we'll send the event there instead
-            }
-        }
-        launch {
-            var prev = LoadStates.IDLE
-            accessor.state.collect {
-                if (prev.refresh != it.refresh) {
-                    dispatchIfValid(REFRESH, it.refresh)
-                }
-                if (prev.prepend != it.prepend) {
-                    dispatchIfValid(PREPEND, it.prepend)
-                }
-                if (prev.append != it.append) {
-                    dispatchIfValid(APPEND, it.append)
-                }
-                prev = it
-            }
-        }
+        accessor: RemoteMediatorAccessor<Key, Value>?
+    ): Flow<PageEvent<Value>> {
+        if (accessor == null) return pageEventFlow
 
-        this@injectRemoteEvents.pageEventFlow.collect {
-            // only insert events have combinedLoadStates.
-            if (it is PageEvent.Insert<Value>) {
-                send(
-                    it.copy(
-                        combinedLoadStates = CombinedLoadStates(
-                            it.combinedLoadStates.source,
-                            accessor.state.value
+        @OptIn(ExperimentalCoroutinesApi::class)
+        return channelFlow {
+            val loadStates = MutableLoadStateCollection()
+
+            suspend fun dispatchIfValid(type: LoadType, state: LoadState) {
+                // not loading events are sent w/ insert-drop events.
+                if (PageEvent.LoadStateUpdate.canDispatchWithoutInsert(
+                        state,
+                        fromMediator = true
+                    )
+                ) {
+                    send(
+                        PageEvent.LoadStateUpdate<Value>(
+                            loadType = type,
+                            fromMediator = true,
+                            loadState = state
                         )
                     )
-                )
-            } else {
-                send(it)
+                } else {
+                    // Wait for invalidation to set state to NotLoading via Insert to prevent any
+                    // potential for flickering.
+                }
+            }
+
+            launch {
+                var prev = LoadStates.IDLE
+                accessor.state.collect {
+                    if (prev.refresh != it.refresh) {
+                        loadStates.set(REFRESH, true, it.refresh)
+                        dispatchIfValid(REFRESH, it.refresh)
+                    }
+                    if (prev.prepend != it.prepend) {
+                        loadStates.set(PREPEND, true, it.prepend)
+                        dispatchIfValid(PREPEND, it.prepend)
+                    }
+                    if (prev.append != it.append) {
+                        loadStates.set(APPEND, true, it.append)
+                        dispatchIfValid(APPEND, it.append)
+                    }
+                    prev = it
+                }
+            }
+
+            this@injectRemoteEvents.pageEventFlow.collect { event ->
+                when (event) {
+                    is PageEvent.Insert -> {
+                        loadStates.set(
+                            sourceLoadStates = event.combinedLoadStates.source,
+                            remoteLoadStates = accessor.state.value
+                        )
+                        send(event.copy(combinedLoadStates = loadStates.snapshot()))
+                    }
+                    is PageEvent.Drop -> {
+                        loadStates.set(
+                            type = event.loadType,
+                            remote = false,
+                            state = LoadState.NotLoading.Incomplete
+                        )
+                        send(event)
+                    }
+                    is PageEvent.LoadStateUpdate -> {
+                        loadStates.set(
+                            type = event.loadType,
+                            remote = event.fromMediator,
+                            state = event.loadState
+                        )
+                        send(event)
+                    }
+                }
             }
         }
     }
@@ -177,11 +208,13 @@
         refreshEvents.send(false)
     }
 
-    private fun generateNewPagingSource(
+    private suspend fun generateNewPagingSource(
         previousPagingSource: PagingSource<Key, Value>?
     ): PagingSource<Key, Value> {
         val pagingSource = pagingSourceFactory()
-
+        if (pagingSource is LegacyPagingSource) {
+            pagingSource.setPageSize(config.pageSize)
+        }
         // Ensure pagingSourceFactory produces a new instance of PagingSource.
         check(pagingSource !== previousPagingSource) {
             """
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
index a641388..020ae17 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
@@ -285,7 +285,6 @@
         key = key,
         loadSize = if (loadType == REFRESH) config.initialLoadSize else config.pageSize,
         placeholdersEnabled = config.enablePlaceholders,
-        pageSize = config.pageSize
     )
 
     private suspend fun doInitialLoad() {
@@ -304,19 +303,13 @@
                     if (result.prevKey == null) {
                         state.setSourceLoadState(
                             type = PREPEND,
-                            newState = when (remoteMediatorConnection) {
-                                null -> NotLoading.Complete
-                                else -> NotLoading.Incomplete
-                            }
+                            newState = NotLoading.Complete
                         )
                     }
                     if (result.nextKey == null) {
                         state.setSourceLoadState(
                             type = APPEND,
-                            newState = when (remoteMediatorConnection) {
-                                null -> NotLoading.Complete
-                                else -> NotLoading.Incomplete
-                            }
+                            newState = NotLoading.Complete
                         )
                     }
                 }
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt
index 147618c..36c0421 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt
@@ -27,7 +27,6 @@
 import androidx.paging.PagingConfig.Companion.MAX_SIZE_UNBOUNDED
 import androidx.paging.PagingSource.LoadResult.Page
 import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.consumeAsFlow
@@ -109,13 +108,11 @@
     internal var sourceLoadStates = LoadStates.IDLE
         private set
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     fun consumePrependGenerationIdAsFlow(): Flow<Int> {
         return prependGenerationIdCh.consumeAsFlow()
             .onStart { prependGenerationIdCh.offer(prependGenerationId) }
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     fun consumeAppendGenerationIdAsFlow(): Flow<Int> {
         return appendGenerationIdCh.consumeAsFlow()
             .onStart { appendGenerationIdCh.offer(appendGenerationId) }
@@ -152,24 +149,33 @@
                 placeholdersBefore = placeholdersBefore,
                 placeholdersAfter = placeholdersAfter,
                 combinedLoadStates = CombinedLoadStates(
+                    refresh = sourceLoadStates.refresh,
+                    prepend = sourceLoadStates.prepend,
+                    append = sourceLoadStates.append,
                     source = sourceLoadStates,
-                    mediator = null
+                    mediator = null,
                 )
             )
             PREPEND -> Prepend(
                 pages = pages,
                 placeholdersBefore = placeholdersBefore,
                 combinedLoadStates = CombinedLoadStates(
+                    refresh = sourceLoadStates.refresh,
+                    prepend = sourceLoadStates.prepend,
+                    append = sourceLoadStates.append,
                     source = sourceLoadStates,
-                    mediator = null
+                    mediator = null,
                 )
             )
             APPEND -> Append(
                 pages = pages,
                 placeholdersAfter = placeholdersAfter,
                 combinedLoadStates = CombinedLoadStates(
+                    refresh = sourceLoadStates.refresh,
+                    prepend = sourceLoadStates.prepend,
+                    append = sourceLoadStates.append,
                     source = sourceLoadStates,
-                    mediator = null
+                    mediator = null,
                 )
             )
         }
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
index 4b472b2..2b22813 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
@@ -26,7 +26,6 @@
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
 import java.lang.ref.WeakReference
 import java.util.AbstractList
 import java.util.concurrent.Executor
@@ -179,12 +178,9 @@
                         key,
                         config.initialLoadSizeHint,
                         config.enablePlaceholders,
-                        config.pageSize
                     )
                     runBlocking {
-                        val initialResult = withContext(DirectDispatcher) {
-                            pagingSource.load(params)
-                        }
+                        val initialResult = pagingSource.load(params)
                         when (initialResult) {
                             is PagingSource.LoadResult.Page -> initialResult
                             is PagingSource.LoadResult.Error -> throw initialResult.throwable
@@ -480,7 +476,14 @@
         @Suppress("DEPRECATION")
         fun build(): PagedList<Value> {
             val fetchDispatcher = fetchDispatcher ?: Dispatchers.IO
-            val pagingSource = pagingSource ?: dataSource?.let { LegacyPagingSource { it } }
+            val pagingSource = pagingSource ?: dataSource?.let { dataSource ->
+                LegacyPagingSource(
+                    fetchDispatcher = fetchDispatcher,
+                    dataSource = dataSource
+                ).also {
+                    it.setPageSize(config.pageSize)
+                }
+            }
 
             check(pagingSource != null) {
                 "PagedList cannot be built without a PagingSource or DataSource"
diff --git a/paging/common/src/main/kotlin/androidx/paging/Pager.kt b/paging/common/src/main/kotlin/androidx/paging/Pager.kt
index ed3cfda..8f2483d 100644
--- a/paging/common/src/main/kotlin/androidx/paging/Pager.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/Pager.kt
@@ -38,22 +38,41 @@
  * `androidx.paging:paging-rxjava2` artifact.
  */
 class Pager<Key : Any, Value : Any>
-@JvmOverloads constructor(
+// Experimental usage is propagated to public API via constructor argument.
+@ExperimentalPagingApi constructor(
     config: PagingConfig,
     initialKey: Key? = null,
-    @OptIn(ExperimentalPagingApi::class)
-    remoteMediator: RemoteMediator<Key, Value>? = null,
+    remoteMediator: RemoteMediator<Key, Value>?,
     pagingSourceFactory: () -> PagingSource<Key, Value>
 ) {
+    // Experimental usage is internal, so opt-in is allowed here.
+    @JvmOverloads
+    @OptIn(ExperimentalPagingApi::class)
+    constructor(
+        config: PagingConfig,
+        initialKey: Key? = null,
+        pagingSourceFactory: () -> PagingSource<Key, Value>
+    ) : this(config, initialKey, null, pagingSourceFactory)
+
     /**
      * A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become
      * invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or
      * [PagingDataAdapter.refresh].
      */
     val flow: Flow<PagingData<Value>> = PageFetcher(
-        pagingSourceFactory,
-        initialKey,
-        config,
-        remoteMediator
+        pagingSourceFactory = if (
+            pagingSourceFactory is SuspendingPagingSourceFactory<Key, Value>
+        ) {
+            pagingSourceFactory::create
+        } else {
+            // cannot pass it as is since it is not a suspend function. Hence, we wrap it in {}
+            // which means we are calling the original factory inside a suspend function
+            {
+                pagingSourceFactory()
+            }
+        },
+        initialKey = initialKey,
+        config = config,
+        remoteMediator = remoteMediator
     ).flow
 }
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingData.kt b/paging/common/src/main/kotlin/androidx/paging/PagingData.kt
index 2f842b7..61548a2 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingData.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingData.kt
@@ -143,13 +143,15 @@
                     placeholdersBefore = 0,
                     placeholdersAfter = 0,
                     combinedLoadStates = CombinedLoadStates(
+                        refresh = LoadState.NotLoading.Incomplete,
+                        prepend = LoadState.NotLoading.Complete,
+                        append = LoadState.NotLoading.Complete,
                         source = LoadStates(
                             refresh = LoadState.NotLoading.Incomplete,
                             prepend = LoadState.NotLoading.Complete,
                             append = LoadState.NotLoading.Complete
                         )
                     )
-
                 )
             ),
             receiver = NOOP_RECEIVER
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt b/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
index 90bb25e..3f93b05 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
@@ -24,7 +24,6 @@
 import androidx.paging.PagePresenter.ProcessPageEventCallback
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -103,6 +102,13 @@
     }
 
     /**
+     * @param onListPresentable Call this synchronously right before dispatching updates to signal
+     * that this [PagingDataDiffer] should now consider [newList] as the presented list for
+     * presenter-level APIs such as [snapshot] and [peek]. This should be called before notifying
+     * any callbacks that the user would expect to be synchronous with presenter updates, such as
+     * `ListUpdateCallback`, in case it's desirable to inspect presenter state within those
+     * callbacks.
+     *
      * @return Transformed result of [lastAccessedIndex] as an index of [newList] using the diff
      * result between [previousList] and [newList]. Null if [newList] or [previousList] lists are
      * empty, where it does not make sense to transform [lastAccessedIndex].
@@ -111,7 +117,8 @@
         previousList: NullPaddedList<T>,
         newList: NullPaddedList<T>,
         newCombinedLoadStates: CombinedLoadStates,
-        lastAccessedIndex: Int
+        lastAccessedIndex: Int,
+        onListPresentable: () -> Unit,
     ): Int?
 
     open fun postEvents(): Boolean = false
@@ -126,13 +133,23 @@
                     lastAccessedIndexUnfulfilled = false
 
                     val newPresenter = PagePresenter(event)
+                    var onListPresentableCalled = false
                     val transformedLastAccessedIndex = presentNewList(
                         previousList = presenter,
                         newList = newPresenter,
                         newCombinedLoadStates = event.combinedLoadStates,
-                        lastAccessedIndex = lastAccessedIndex
+                        lastAccessedIndex = lastAccessedIndex,
+                        onListPresentable = {
+                            presenter = newPresenter
+                            onListPresentableCalled = true
+                        }
                     )
-                    presenter = newPresenter
+                    check(onListPresentableCalled) {
+                        "Missing call to onListPresentable after new list was presented. If you " +
+                            "are seeing this exception, it is generally an indication of an " +
+                            "issue with Paging. Please file a bug so we can fix it at: " +
+                            "https://issuetracker.google.com/issues/new?component=413106"
+                    }
 
                     // Dispatch LoadState updates as soon as we are done diffing, but after setting
                     // presenter.
@@ -277,7 +294,6 @@
     val size: Int
         get() = presenter.size
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     private val _combinedLoadState = MutableStateFlow(combinedLoadStates.snapshot())
 
     /**
@@ -294,7 +310,6 @@
         get() = _combinedLoadState
 
     init {
-        @OptIn(ExperimentalCoroutinesApi::class)
         addLoadStateListener {
             _combinedLoadState.value = it
         }
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
index 345b7cf..4db4e26 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
@@ -32,7 +32,6 @@
         key,
         initialLoadSizeHint,
         enablePlaceholders,
-        pageSize
     )
 
 /**
@@ -100,15 +99,6 @@
          * [LoadResult.Page.itemsAfter] if possible.
          */
         val placeholdersEnabled: Boolean,
-        /**
-         * From [PagingConfig.pageSize], the configured page size.
-         */
-        @Deprecated(
-            message = "PagingConfig.pageSize will be removed in future versions, use " +
-                "PagingConfig.loadSize instead.",
-            replaceWith = ReplaceWith("loadSize")
-        )
-        val pageSize: Int = loadSize
     ) {
         /**
          * Key for the page to be loaded.
@@ -131,45 +121,39 @@
          * Params for an initial load request on a [PagingSource] from [PagingSource.load] or a
          * refresh triggered by [invalidate].
          */
-        class Refresh<Key : Any> @JvmOverloads constructor(
+        class Refresh<Key : Any> constructor(
             override val key: Key?,
             loadSize: Int,
             placeholdersEnabled: Boolean,
-            pageSize: Int = loadSize
         ) : LoadParams<Key>(
             loadSize = loadSize,
             placeholdersEnabled = placeholdersEnabled,
-            pageSize = pageSize
         )
 
         /**
          * Params to load a page of data from a [PagingSource] via [PagingSource.load] to be
          * appended to the end of the list.
          */
-        class Append<Key : Any> @JvmOverloads constructor(
+        class Append<Key : Any> constructor(
             override val key: Key,
             loadSize: Int,
             placeholdersEnabled: Boolean,
-            pageSize: Int = loadSize
         ) : LoadParams<Key>(
             loadSize = loadSize,
             placeholdersEnabled = placeholdersEnabled,
-            pageSize = pageSize
         )
 
         /**
          * Params to load a page of data from a [PagingSource] via [PagingSource.load] to be
          * prepended to the start of the list.
          */
-        class Prepend<Key : Any> @JvmOverloads constructor(
+        class Prepend<Key : Any> constructor(
             override val key: Key,
             loadSize: Int,
             placeholdersEnabled: Boolean,
-            pageSize: Int = loadSize
         ) : LoadParams<Key>(
             loadSize = loadSize,
             placeholdersEnabled = placeholdersEnabled,
-            pageSize = pageSize
         )
 
         internal companion object {
@@ -178,13 +162,11 @@
                 key: Key?,
                 loadSize: Int,
                 placeholdersEnabled: Boolean,
-                pageSize: Int
             ): LoadParams<Key> = when (loadType) {
                 LoadType.REFRESH -> Refresh(
                     key = key,
                     loadSize = loadSize,
                     placeholdersEnabled = placeholdersEnabled,
-                    pageSize = pageSize
                 )
                 LoadType.PREPEND -> Prepend(
                     loadSize = loadSize,
@@ -192,7 +174,6 @@
                         "key cannot be null for prepend"
                     },
                     placeholdersEnabled = placeholdersEnabled,
-                    pageSize = pageSize
                 )
                 LoadType.APPEND -> Append(
                     loadSize = loadSize,
@@ -200,7 +181,6 @@
                         "key cannot be null for append"
                     },
                     placeholdersEnabled = placeholdersEnabled,
-                    pageSize = pageSize
                 )
             }
         }
@@ -335,7 +315,6 @@
      * list of loaded pages is not empty. In the case where a refresh is triggered before the
      * initial load succeeds or it errors out, the initial key passed to [Pager] will be used.
      */
-    @ExperimentalPagingApi
     open fun getRefreshKey(state: PagingState<Key, Value>): Key? = null
 
     private val onInvalidatedCallbacks = CopyOnWriteArrayList<() -> Unit>()
@@ -355,9 +334,7 @@
      * This method is idempotent. i.e., If [invalidate] has already been called, subsequent calls to
      * this method should have no effect.
      */
-    open fun invalidate() {
-        // TODO(b/137971356): Investigate making this not open when able to remove
-        //  LegacyPagingSource.
+    fun invalidate() {
         if (_invalid.compareAndSet(false, true)) {
             onInvalidatedCallbacks.forEach { it.invoke() }
         }
diff --git a/paging/common/src/main/kotlin/androidx/paging/Separators.kt b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
index 94ed298..632f888 100644
--- a/paging/common/src/main/kotlin/androidx/paging/Separators.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
@@ -16,10 +16,13 @@
 
 package androidx.paging
 
+import androidx.paging.LoadState.NotLoading
 import androidx.paging.LoadType.APPEND
 import androidx.paging.LoadType.PREPEND
+import androidx.paging.LoadType.REFRESH
 import androidx.paging.PageEvent.Drop
 import androidx.paging.PageEvent.Insert
+import androidx.paging.PageEvent.LoadStateUpdate
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 
@@ -165,17 +168,18 @@
     var endTerminalSeparatorDeferred = false
     var startTerminalSeparatorDeferred = false
 
+    val loadStates = MutableLoadStateCollection()
+    var placeholdersBefore = 0
+    var placeholdersAfter = 0
+
     var footerAdded = false
     var headerAdded = false
 
     @Suppress("UNCHECKED_CAST")
     suspend fun onEvent(event: PageEvent<T>): PageEvent<R> = when (event) {
         is Insert<T> -> onInsert(event)
-        is Drop -> {
-            onDrop(event) // Update pageStash state
-            event as Drop<R>
-        }
-        is PageEvent.LoadStateUpdate -> event as PageEvent<R>
+        is Drop -> onDrop(event)
+        is LoadStateUpdate -> onLoadStateUpdate(event)
     }.also {
         // validate internal state after each modification
         if (endTerminalSeparatorDeferred) {
@@ -191,16 +195,14 @@
         return this as Insert<R>
     }
 
-    fun LoadState.isTerminal(): Boolean {
-        return this is LoadState.NotLoading && endOfPaginationReached
-    }
-
     fun CombinedLoadStates.terminatesStart(): Boolean {
-        return source.prepend.isTerminal() && mediator?.prepend?.isTerminal() != false
+        return source.prepend.endOfPaginationReached &&
+            mediator?.prepend?.endOfPaginationReached != false
     }
 
     fun CombinedLoadStates.terminatesEnd(): Boolean {
-        return source.append.isTerminal() && mediator?.append?.isTerminal() != false
+        return source.append.endOfPaginationReached &&
+            mediator?.append?.endOfPaginationReached != false
     }
 
     fun <T : Any> Insert<T>.terminatesStart(): Boolean = if (loadType == APPEND) {
@@ -227,6 +229,17 @@
             "Additional append event after append state is done"
         }
 
+        // Update SeparatorState before we do any real work.
+        loadStates.set(event.combinedLoadStates)
+        // Append insert has placeholdersBefore = -1 as a placeholder value.
+        if (event.loadType != APPEND) {
+            placeholdersBefore = event.placeholdersBefore
+        }
+        // Prepend insert has placeholdersAfter = -1 as a placeholder value.
+        if (event.loadType != PREPEND) {
+            placeholdersAfter = event.placeholdersAfter
+        }
+
         if (eventEmpty) {
             if (eventTerminatesStart && eventTerminatesEnd) {
                 // if event is empty, and fully terminal, resolve single separator, and that's it
@@ -410,7 +423,16 @@
     /**
      * Process a [Drop] event to update [pageStash] stage.
      */
-    fun onDrop(event: Drop<T>) {
+    fun onDrop(event: Drop<T>): Drop<R> {
+        loadStates.set(type = event.loadType, remote = false, state = NotLoading.Incomplete)
+        if (event.loadType == PREPEND) {
+            placeholdersBefore = event.placeholdersRemaining
+            headerAdded = false
+        } else if (event.loadType == APPEND) {
+            placeholdersAfter = event.placeholdersRemaining
+            footerAdded = false
+        }
+
         if (pageStash.isEmpty()) {
             if (event.loadType == PREPEND) {
                 startTerminalSeparatorDeferred = false
@@ -424,6 +446,49 @@
         pageStash.removeAll { stash ->
             stash.originalPageOffsets.any { pageOffsetsToDrop.contains(it) }
         }
+
+        @Suppress("UNCHECKED_CAST")
+        return event as Drop<R>
+    }
+
+    suspend fun onLoadStateUpdate(event: LoadStateUpdate<T>): PageEvent<R> {
+        // Check for redundant LoadStateUpdate events to avoid unnecessary mapping to empty inserts
+        // that might cause terminal separators to get added out of place.
+        if (loadStates.get(event.loadType, event.fromMediator) == event.loadState) {
+            @Suppress("UNCHECKED_CAST")
+            return event as PageEvent<R>
+        }
+
+        loadStates.set(type = event.loadType, remote = event.fromMediator, state = event.loadState)
+
+        // Transform terminal load state updates into empty inserts for header + footer support
+        // when used with RemoteMediator. In cases where we defer adding a terminal separator,
+        // RemoteMediator can report endOfPaginationReached via LoadStateUpdate event, which
+        // isn't possible to add a separator to. Note: Adding a separate insert event also
+        // doesn't work in the case where .insertSeparators() is called multiple times on the
+        // same page event stream - we have to transform the terminating LoadStateUpdate event.
+        if (event.loadType != REFRESH && event.fromMediator &&
+            event.loadState.endOfPaginationReached
+        ) {
+            val emptyTerminalInsert: Insert<T> = if (event.loadType == PREPEND) {
+                Insert.Prepend(
+                    pages = emptyList(),
+                    placeholdersBefore = placeholdersBefore,
+                    combinedLoadStates = loadStates.snapshot(),
+                )
+            } else {
+                Insert.Append(
+                    pages = emptyList(),
+                    placeholdersAfter = placeholdersAfter,
+                    combinedLoadStates = loadStates.snapshot(),
+                )
+            }
+
+            return onInsert(emptyTerminalInsert)
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        return event as PageEvent<R>
     }
 
     private fun <T : Any> transformablePageToStash(
diff --git a/paging/common/src/main/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt b/paging/common/src/main/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt
new file mode 100644
index 0000000..1b15542f
--- /dev/null
+++ b/paging/common/src/main/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * Utility class to convert the paging source factory to a suspend one.
+ *
+ * This is internal because it is only necessary for the legacy paging source implementation
+ * where the data source must be created on the given thread pool for API guarantees.
+ * see: b/173029013
+ * see: b/168061354
+ */
+internal class SuspendingPagingSourceFactory<Key : Any, Value : Any>(
+    private val dispatcher: CoroutineDispatcher,
+    private val delegate: () -> PagingSource<Key, Value>
+) : () -> PagingSource<Key, Value> {
+    suspend fun create(): PagingSource<Key, Value> {
+        return withContext(dispatcher) {
+            delegate()
+        }
+    }
+
+    override fun invoke(): PagingSource<Key, Value> {
+        return delegate()
+    }
+}
diff --git a/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt b/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt
index 18d85e5..b2073b5 100644
--- a/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt
@@ -17,7 +17,6 @@
 package androidx.paging.multicast
 
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.consumeAsFlow
@@ -72,8 +71,7 @@
         )
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
-    val flow = flow<T> {
+    val flow: Flow<T> = flow {
         val channel = Channel<ChannelManager.Message.Dispatch.Value<T>>(Channel.UNLIMITED)
         val subFlow = channel.consumeAsFlow()
             .onStart {
diff --git a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
index d83ec83..03811c2 100644
--- a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 @file:Suppress("DEPRECATION")
 
 package androidx.paging
@@ -62,9 +61,9 @@
      * and alignment restrictions. These tests were written before positional+contiguous enforced
      * these behaviors.
      */
-    private inner class TestPagingSource(val listData: List<Item> = ITEMS) :
-        PagingSource<Int, Item>() {
-        @OptIn(ExperimentalPagingApi::class)
+    private inner class TestPagingSource(
+        val listData: List<Item> = ITEMS
+    ) : PagingSource<Int, Item>() {
         override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
             return state.anchorPosition
                 ?.let { anchorPosition -> state.closestItemToPosition(anchorPosition)?.pos }
@@ -189,15 +188,13 @@
 
     private fun PagingSource<Int, Item>.getInitialPage(
         initialKey: Int,
-        loadSize: Int,
-        pageSize: Int
+        loadSize: Int
     ): Page<Int, Item> = runBlocking {
         val result = load(
             PagingSource.LoadParams.Refresh(
                 initialKey,
                 loadSize,
                 placeholdersEnabled,
-                pageSize
             )
         )
 
@@ -216,8 +213,7 @@
     ): PagedList<Item> {
         val initialPage = pagingSource.getInitialPage(
             initialPosition ?: 0,
-            initLoadSize,
-            pageSize
+            initLoadSize
         )
 
         val config = Config.Builder()
@@ -316,10 +312,6 @@
         }
     }
 
-    private fun verifyDropCallback(callback: Callback, position: Int) {
-        verifyDropCallback(callback, position, position)
-    }
-
     @Test
     fun append() {
         val pagedList = createCountedPagedList(0)
@@ -466,7 +458,7 @@
         drain()
         verifyRange(20, 60, pagedList)
         verifyCallback(callback, 60)
-        verifyDropCallback(callback, 0)
+        verifyDropCallback(callback, 0, 0)
         verifyNoMoreInteractions(callback)
     }
 
@@ -1053,10 +1045,10 @@
 
         assertTrue { mainThread.queue.isEmpty() }
 
-        pagedList.dispatchStateChangeAsync(LoadType.REFRESH, LoadState.Loading)
+        pagedList.dispatchStateChangeAsync(LoadType.REFRESH, Loading)
         assertEquals(1, mainThread.queue.size)
 
-        pagedList.dispatchStateChangeAsync(LoadType.REFRESH, LoadState.NotLoading.Incomplete)
+        pagedList.dispatchStateChangeAsync(LoadType.REFRESH, NotLoading.Incomplete)
         assertEquals(2, mainThread.queue.size)
     }
 
diff --git a/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
index 5448b6d..13182ab 100644
--- a/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
@@ -19,6 +19,7 @@
 import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
 import com.nhaarman.mockitokotlin2.capture
 import com.nhaarman.mockitokotlin2.mock
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
@@ -289,7 +290,7 @@
         @Suppress("DEPRECATION")
         PagedList.Builder(dataSource, 10)
             .setNotifyDispatcher(FailDispatcher())
-            .setFetchDispatcher(DirectDispatcher)
+            .setFetchDispatcher(Dispatchers.IO)
             .setInitialKey("")
             .build()
     }
diff --git a/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt b/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
index a9e2c3d..6e8ede2 100644
--- a/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
@@ -122,7 +122,6 @@
                     key = start,
                     loadSize = end - start,
                     placeholdersEnabled = config.enablePlaceholders,
-                    pageSize = config.pageSize
                 )
             )
         }
@@ -140,7 +139,7 @@
             GlobalScope,
             config,
             pagingSource,
-            DirectDispatcher,
+            testDispatcher,
             testDispatcher,
             consumer,
             storage as LegacyPageFetcher.KeyProvider<Int>
diff --git a/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
index 65a648b..add2986 100644
--- a/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
@@ -17,20 +17,23 @@
 package androidx.paging
 
 import androidx.paging.PagingSource.LoadResult.Page
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.Runnable
-import kotlinx.coroutines.launch
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.runBlocking
 import org.junit.Assert
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import kotlin.coroutines.CoroutineContext
+import java.util.concurrent.Executors
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
-@OptIn(ExperimentalPagingApi::class)
 @RunWith(JUnit4::class)
 class LegacyPagingSourceTest {
     private val fakePagingState = PagingState(
@@ -76,7 +79,10 @@
 
             override fun getKey(item: String) = item.hashCode()
         }
-        val pagingSource = LegacyPagingSource { dataSource }
+        val pagingSource = LegacyPagingSource(
+            fetchDispatcher = Dispatchers.Unconfined,
+            dataSource
+        )
 
         // Check that jumpingSupported is disabled.
         assertFalse { pagingSource.jumpingSupported }
@@ -119,7 +125,10 @@
                 Assert.fail("loadAfter not expected")
             }
         }
-        val pagingSource = LegacyPagingSource { dataSource }
+        val pagingSource = LegacyPagingSource(
+            fetchDispatcher = Dispatchers.Unconfined,
+            dataSource = dataSource
+        )
 
         // Check that jumpingSupported is disabled.
         assertFalse { pagingSource.jumpingSupported }
@@ -139,7 +148,10 @@
 
     @Test
     fun positional() {
-        val pagingSource = LegacyPagingSource { createTestPositionalDataSource() }
+        val pagingSource = LegacyPagingSource(
+            fetchDispatcher = Dispatchers.Unconfined,
+            dataSource = createTestPositionalDataSource()
+        )
 
         // Check that jumpingSupported is enabled.
         assertTrue { pagingSource.jumpingSupported }
@@ -189,7 +201,10 @@
 
     @Test
     fun invalidateFromPagingSource() {
-        val pagingSource = LegacyPagingSource { createTestPositionalDataSource() }
+        val pagingSource = LegacyPagingSource(
+            fetchDispatcher = Dispatchers.Unconfined,
+            dataSource = createTestPositionalDataSource()
+        )
         val dataSource = pagingSource.dataSource
 
         var kotlinInvalidated = false
@@ -216,7 +231,10 @@
 
     @Test
     fun invalidateFromDataSource() {
-        val pagingSource = LegacyPagingSource { createTestPositionalDataSource() }
+        val pagingSource = LegacyPagingSource(
+            fetchDispatcher = Dispatchers.Unconfined,
+            dataSource = createTestPositionalDataSource()
+        )
         val dataSource = pagingSource.dataSource
 
         var kotlinInvalidated = false
@@ -241,41 +259,57 @@
         assertTrue { javaInvalidated }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun createDataSourceOnFetchDispatcher() {
-        val manualDispatcher = object : CoroutineDispatcher() {
-            val coroutines = ArrayList<Pair<CoroutineContext, Runnable>>()
-            override fun dispatch(context: CoroutineContext, block: Runnable) {
-                coroutines.add(context to block)
+        val methodCalls = mutableMapOf<String, MutableList<Thread>>()
+
+        val dataSourceFactory = object : DataSource.Factory<Int, String>() {
+            override fun create(): DataSource<Int, String> {
+                return ThreadCapturingDataSource { methodName ->
+                    methodCalls.getOrPut(methodName) {
+                        mutableListOf()
+                    }.add(Thread.currentThread())
+                }
             }
         }
 
-        var initialized = false
-        val pagingSource = LegacyPagingSource(manualDispatcher) {
-            initialized = true
-            createTestPositionalDataSource(expectInitialLoad = true)
-        }
+        // create an executor special to the legacy data source
+        val executor = Executors.newSingleThreadExecutor()
 
-        assertFalse { initialized }
+        // extract the thread instance from the executor. we'll use it to assert calls later
+        var dataSourceThread: Thread? = null
+        executor.submit {
+            dataSourceThread = Thread.currentThread()
+        }.get()
 
-        // Trigger lazy-initialization dispatch.
-        val job = GlobalScope.launch {
-            pagingSource.load(PagingSource.LoadParams.Refresh(0, 1, false, 1))
-        }
-
-        // Assert that initialization has been scheduled on manualDispatcher, which has not been
-        // triggered yet.
-        assertFalse { initialized }
-
-        // Force all tasks on manualDispatcher to run.
-        while (!job.isCompleted) {
-            while (manualDispatcher.coroutines.isNotEmpty()) {
-                @OptIn(ExperimentalStdlibApi::class)
-                manualDispatcher.coroutines.removeFirst().second.run()
+        val pager = Pager(
+            config = PagingConfig(10, enablePlaceholders = false),
+            pagingSourceFactory = dataSourceFactory.asPagingSourceFactory(
+                executor.asCoroutineDispatcher()
+            )
+        )
+        // collect from pager. we take only 2 paging data generations and only take 1 PageEvent
+        // from them
+        runBlocking {
+            pager.flow.take(2).collectLatest { pagingData ->
+                // wait until first insert happens
+                pagingData.flow.filter {
+                    it is PageEvent.Insert
+                }.first()
+                pagingData.receiver.refresh()
             }
         }
-
-        assertTrue { initialized }
+        // validate method calls (to ensure test did run as expected) and their threads.
+        assertThat(methodCalls["<init>"]).hasSize(2)
+        assertThat(methodCalls["<init>"]?.toSet()).containsExactly(dataSourceThread)
+        assertThat(methodCalls["addInvalidatedCallback"]).hasSize(2)
+        assertThat(methodCalls["addInvalidatedCallback"]?.toSet()).containsExactly(dataSourceThread)
+        assertThat(methodCalls["loadInitial"]).hasSize(2)
+        assertThat(methodCalls).containsKey("isInvalid")
+        assertThat(methodCalls["loadInitial"]?.toSet()).containsExactly(dataSourceThread)
+        // TODO b/174625633 this should also be 2
+        assertThat(methodCalls["removeInvalidatedCallback"]).hasSize(1)
     }
 
     @Test
@@ -333,4 +367,55 @@
                 Assert.fail("loadRange not expected")
             }
         }
+
+    /**
+     * A data source implementation which tracks method calls and their threads.
+     */
+    @Suppress("DEPRECATION")
+    class ThreadCapturingDataSource(
+        private val recordMethodCall: (methodName: String) -> Unit
+    ) : PositionalDataSource<String>() {
+        init {
+            recordMethodCall("<init>")
+        }
+
+        override fun loadInitial(
+            params: LoadInitialParams,
+            callback: LoadInitialCallback<String>
+        ) {
+            recordMethodCall("loadInitial")
+            callback.onResult(
+                data = emptyList(),
+                position = 0,
+            )
+        }
+
+        override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<String>) {
+            recordMethodCall("loadRange")
+            callback.onResult(data = emptyList())
+        }
+
+        override val isInvalid: Boolean
+            get() {
+                // this is important because room's implementation might run a db query to
+                // update invalidations.
+                recordMethodCall("isInvalid")
+                return super.isInvalid
+            }
+
+        override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            recordMethodCall("addInvalidatedCallback")
+            super.addInvalidatedCallback(onInvalidatedCallback)
+        }
+
+        override fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+            recordMethodCall("removeInvalidatedCallback")
+            super.removeInvalidatedCallback(onInvalidatedCallback)
+        }
+
+        override fun invalidate() {
+            recordMethodCall("invalidate")
+            super.invalidate()
+        }
+    }
 }
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
index 21736e3..232c4f7 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
@@ -72,11 +72,12 @@
 class PageFetcherSnapshotTest {
     private val testScope = TestCoroutineScope()
     private val retryBus = ConflatedEventBus<Unit>()
-    private val pagingSourceFactory = {
-        TestPagingSource().also {
+    private val pagingSourceFactory = suspend {
+        TestPagingSource(loadDelay = 1000).also {
             currentPagingSource = it
         }
     }
+
     private var currentPagingSource: TestPagingSource? = null
     private val config = PagingConfig(
         pageSize = 1,
@@ -1752,7 +1753,7 @@
         }
 
         var createdPagingSource = false
-        val factory = {
+        val factory = suspend {
             check(!createdPagingSource)
             createdPagingSource = true
             TestPagingSource(items = List(2) { it })
@@ -1801,7 +1802,7 @@
         }
 
         var createdPagingSource = false
-        val factory = {
+        val factory = suspend {
             check(!createdPagingSource)
             createdPagingSource = true
             TestPagingSource(items = List(2) { it })
@@ -2183,7 +2184,14 @@
                 LoadStateUpdate(REFRESH, true, Loading),
                 LoadStateUpdate(REFRESH, true, Error(EXCEPTION)),
                 LoadStateUpdate(REFRESH, false, Loading),
-                createRefresh(0..2, remoteLoadStatesOf(refreshRemote = Error(EXCEPTION))),
+                createRefresh(
+                    range = 0..2,
+                    remoteLoadStatesOf(
+                        refresh = Error(EXCEPTION),
+                        prependLocal = NotLoading.Complete,
+                        refreshRemote = Error(EXCEPTION),
+                    ),
+                ),
                 // since remote refresh failed and launch initial refresh is requested,
                 // we won't receive any append/prepend events
             )
@@ -2262,6 +2270,79 @@
     }
 
     @Test
+    fun remoteMediator_remoteRefreshEndOfPaginationReached() = testScope.runBlockingTest {
+        @OptIn(ExperimentalPagingApi::class)
+        val remoteMediator = RemoteMediatorMock().apply {
+            initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
+            loadCallback = { _, _ -> RemoteMediator.MediatorResult.Success(true) }
+        }
+
+        val config = PagingConfig(
+            pageSize = 1,
+            prefetchDistance = 2,
+            enablePlaceholders = true,
+            initialLoadSize = 1,
+            maxSize = 5
+        )
+        val pager = PageFetcher(
+            initialKey = 0,
+            pagingSourceFactory = { TestPagingSource(items = listOf(0)) },
+            config = config,
+            remoteMediator = remoteMediator
+        )
+
+        val state = collectFetcherState(pager)
+
+        advanceUntilIdle()
+        assertThat(state.newEvents()).isEqualTo(
+            listOf(
+                LoadStateUpdate(loadType = REFRESH, fromMediator = true, loadState = Loading),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                LoadStateUpdate(
+                    loadType = PREPEND,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                LoadStateUpdate(
+                    loadType = APPEND,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                LoadStateUpdate(loadType = REFRESH, fromMediator = false, loadState = Loading),
+                Refresh(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf(0),
+                            hintOriginalPageOffset = 0,
+                            hintOriginalIndices = null
+                        )
+                    ),
+                    placeholdersBefore = 0,
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        refresh = NotLoading(endOfPaginationReached = true),
+                        prepend = NotLoading(endOfPaginationReached = true),
+                        append = NotLoading(endOfPaginationReached = true),
+                        refreshLocal = NotLoading(endOfPaginationReached = false),
+                        prependLocal = NotLoading(endOfPaginationReached = true),
+                        appendLocal = NotLoading(endOfPaginationReached = true),
+                        refreshRemote = NotLoading(endOfPaginationReached = true),
+                        prependRemote = NotLoading(endOfPaginationReached = true),
+                        appendRemote = NotLoading(endOfPaginationReached = true),
+                    )
+                )
+            )
+        )
+
+        state.job.cancel()
+    }
+
+    @Test
     fun remoteMediator_endOfPaginationNotReachedLoadStatePrepend() = testScope.runBlockingTest {
         @OptIn(ExperimentalPagingApi::class)
         val remoteMediator = object : RemoteMediatorMock() {
@@ -2305,7 +2386,9 @@
                     ),
                     placeholdersBefore = 0,
                     placeholdersAfter = 99,
-                    combinedLoadStates = remoteLoadStatesOf()
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete
+                    )
                 ),
                 LoadStateUpdate(
                     loadType = PREPEND,
@@ -2328,7 +2411,9 @@
                     ),
                     placeholdersBefore = 0,
                     placeholdersAfter = 99,
-                    combinedLoadStates = remoteLoadStatesOf()
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete
+                    )
                 )
             )
 
@@ -2380,7 +2465,9 @@
                         ),
                         placeholdersBefore = 0,
                         placeholdersAfter = 99,
-                        combinedLoadStates = remoteLoadStatesOf()
+                        combinedLoadStates = remoteLoadStatesOf(
+                            prependLocal = NotLoading.Complete,
+                        )
                     ),
                     LoadStateUpdate(
                         loadType = PREPEND,
@@ -2444,10 +2531,7 @@
                     combinedLoadStates = remoteLoadStatesOf()
                 )
             )
-            assertEvents(
-                eventsByGeneration[0],
-                refreshEvents
-            )
+            assertThat(eventsByGeneration[0]).isEqualTo(refreshEvents)
             accessHint(
                 ViewportHint.Access(
                     pageOffset = 0,
@@ -2485,7 +2569,7 @@
                     loadType = PREPEND,
                     fromMediator = true,
                     loadState = NotLoading.Complete
-                )
+                ),
             )
             awaitEventCount(refreshEvents.size + postHintEvents.size)
             assertEquals(
@@ -2539,7 +2623,9 @@
                         ),
                         placeholdersBefore = 99,
                         placeholdersAfter = 0,
-                        combinedLoadStates = remoteLoadStatesOf()
+                        combinedLoadStates = remoteLoadStatesOf(
+                            appendLocal = NotLoading.Complete,
+                        )
                     ),
                     LoadStateUpdate(
                         loadType = APPEND,
@@ -2562,7 +2648,9 @@
                         ),
                         placeholdersBefore = 99,
                         placeholdersAfter = 0,
-                        combinedLoadStates = remoteLoadStatesOf()
+                        combinedLoadStates = remoteLoadStatesOf(
+                            appendLocal = NotLoading.Complete,
+                        )
                     ),
                 )
             )
@@ -2597,7 +2685,7 @@
         )
 
         val expected = listOf(
-            listOf<PageEvent<Int>>(
+            listOf(
                 LoadStateUpdate(
                     loadType = REFRESH,
                     fromMediator = false,
@@ -2612,7 +2700,9 @@
                     ),
                     placeholdersBefore = 99,
                     placeholdersAfter = 0,
-                    combinedLoadStates = remoteLoadStatesOf()
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                    )
                 ),
                 LoadStateUpdate(
                     loadType = APPEND,
@@ -2716,7 +2806,7 @@
                 ),
             )
             awaitEventCount(initialEvents.size + postHintEvents.size)
-            assertEvents(initialEvents + postHintEvents, eventsByGeneration[0])
+            assertThat(eventsByGeneration[0]).isEqualTo(initialEvents + postHintEvents)
         }
     }
 
@@ -2760,8 +2850,13 @@
                         fromMediator = true,
                         loadState = Loading
                     ),
+                    LoadStateUpdate(
+                        loadType = REFRESH,
+                        fromMediator = true,
+                        loadState = NotLoading.Incomplete
+                    ),
                 ),
-                listOf<PageEvent<Int>>(
+                listOf(
                     LoadStateUpdate(
                         loadType = REFRESH,
                         fromMediator = false,
@@ -2786,7 +2881,7 @@
     @Test
     fun remoteMediator_initialRefreshSuccessEndOfPagination() = testScope.runBlockingTest {
         @OptIn(ExperimentalPagingApi::class)
-        val remoteMediator = object : RemoteMediatorMock() {
+        val remoteMediator = object : RemoteMediatorMock(loadDelay = 2000) {
             override suspend fun initialize(): InitializeAction {
                 super.initialize()
                 return InitializeAction.LAUNCH_INITIAL_REFRESH
@@ -2810,57 +2905,79 @@
         )
         val pager = PageFetcher(
             initialKey = 50,
-            pagingSourceFactory = pagingSourceFactory,
+            pagingSourceFactory = {
+                TestPagingSource().apply {
+                    nextLoadResult = Page(
+                        data = listOf(50),
+                        prevKey = null,
+                        nextKey = null,
+                        itemsBefore = 50,
+                        itemsAfter = 49
+                    )
+                }
+            },
             config = config,
             remoteMediator = remoteMediator
         )
 
-        pager.assertEventByGeneration(
+        val fetcherState = collectFetcherState(pager)
+
+        advanceTimeBy(1000)
+
+        assertThat(fetcherState.newEvents()).isEqualTo(
             listOf(
-                listOf(
-                    LoadStateUpdate(
-                        loadType = REFRESH,
-                        fromMediator = true,
-                        loadState = Loading,
-                    ),
-                    LoadStateUpdate(
-                        loadType = REFRESH,
-                        fromMediator = true,
-                        loadState = NotLoading.Complete,
-                    ),
-                    LoadStateUpdate(
-                        loadType = PREPEND,
-                        fromMediator = true,
-                        loadState = NotLoading.Complete,
-                    ),
-                    LoadStateUpdate(
-                        loadType = APPEND,
-                        fromMediator = true,
-                        loadState = NotLoading.Complete,
-                    ),
-                    LoadStateUpdate(
-                        loadType = REFRESH,
-                        fromMediator = false,
-                        loadState = Loading,
-                    ),
-                    Refresh(
-                        pages = listOf(
-                            TransformablePage(
-                                originalPageOffset = 0,
-                                data = listOf(50)
-                            )
-                        ),
-                        placeholdersBefore = 50,
-                        placeholdersAfter = 49,
-                        combinedLoadStates = remoteLoadStatesOf(
-                            refreshRemote = NotLoading.Complete,
-                            prependRemote = NotLoading.Complete,
-                            appendRemote = NotLoading.Complete,
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = Loading,
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = false,
+                    loadState = Loading,
+                ),
+                Refresh(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = 0,
+                            data = listOf(50)
                         )
                     ),
+                    placeholdersBefore = 50,
+                    placeholdersAfter = 49,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        refresh = Loading,
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        refreshRemote = Loading,
+                    )
+                ),
+            ),
+        )
+
+        advanceUntilIdle()
+
+        assertThat(fetcherState.newEvents()).isEqualTo(
+            listOf<PageEvent<Int>>(
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = NotLoading.Complete,
+                ),
+                LoadStateUpdate(
+                    loadType = PREPEND,
+                    fromMediator = true,
+                    loadState = NotLoading.Complete
+                ),
+                LoadStateUpdate(
+                    loadType = APPEND,
+                    fromMediator = true,
+                    loadState = NotLoading.Complete
                 ),
             )
         )
+
+        fetcherState.job.cancel()
     }
 
     @Test
@@ -3225,15 +3342,6 @@
         assertFalse { initialHint.shouldPrioritizeOver(accessHint, APPEND) }
     }
 
-    @OptIn(ExperimentalPagingApi::class)
-    private suspend fun <Key : Any, Value : Any> createRemoteMediatorAccessor(
-        delegate: RemoteMediator<Key, Value>
-    ): RemoteMediatorAccessor<Key, Value> {
-        return RemoteMediatorAccessor(testScope, delegate).also {
-            it.initialize()
-        }
-    }
-
     internal class CollectedPageEvents<T : Any>(val pageEvents: ArrayList<PageEvent<T>>) {
         var lastIndex = 0
         fun newEvents(): List<PageEvent<T>> = when {
@@ -3309,7 +3417,7 @@
             stop()
         }
         expected.forEachIndexed { index, list ->
-            assertEvents(list, actual.getOrNull(index) ?: emptyList())
+            assertThat(actual.getOrNull(index) ?: emptyList<PageEvent<T>>()).isEqualTo(list)
         }
         assertThat(actual.size).isEqualTo(expected.size)
     }
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
index b347092..4cb76d0 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
@@ -49,7 +49,7 @@
 @RunWith(JUnit4::class)
 class PageFetcherTest {
     private val testScope = TestCoroutineScope()
-    private val pagingSourceFactory = { TestPagingSource() }
+    private val pagingSourceFactory = suspend { TestPagingSource() }
     private val config = PagingConfig(
         pageSize = 1,
         prefetchDistance = 1,
@@ -91,7 +91,9 @@
     @Test
     fun refresh_fromPagingSource() = testScope.runBlockingTest {
         var pagingSource: PagingSource<Int, Int>? = null
-        val pagingSourceFactory = { TestPagingSource().also { pagingSource = it } }
+        val pagingSourceFactory = suspend {
+            TestPagingSource().also { pagingSource = it }
+        }
         val pageFetcher = PageFetcher(pagingSourceFactory, 50, config)
         val fetcherState = collectFetcherState(pageFetcher)
 
@@ -114,7 +116,9 @@
     @Test
     fun refresh_callsInvalidate() = testScope.runBlockingTest {
         var pagingSource: PagingSource<Int, Int>? = null
-        val pagingSourceFactory = { TestPagingSource().also { pagingSource = it } }
+        val pagingSourceFactory = suspend {
+            TestPagingSource().also { pagingSource = it }
+        }
         val pageFetcher = PageFetcher(pagingSourceFactory, 50, config)
         val fetcherState = collectFetcherState(pageFetcher)
 
@@ -303,7 +307,9 @@
     fun jump() = testScope.runBlockingTest {
         pauseDispatcher {
             val pagingSources = mutableListOf<PagingSource<Int, Int>>()
-            val pagingSourceFactory = { TestPagingSource().also { pagingSources.add(it) } }
+            val pagingSourceFactory = suspend {
+                TestPagingSource().also { pagingSources.add(it) }
+            }
             val config = PagingConfig(
                 pageSize = 1,
                 prefetchDistance = 1,
@@ -382,7 +388,11 @@
                 prefetchDistance = 1,
                 initialLoadSize = 2
             )
-            val pageFetcher = PageFetcher({ pagingSource }, 50, config)
+            val pageFetcher = PageFetcher(
+                pagingSourceFactory = suspend { pagingSource },
+                initialKey = 50,
+                config = config
+            )
             val job = testScope.launch {
                 assertFailsWith<IllegalStateException> {
                     pageFetcher.flow.collect { }
@@ -444,7 +454,7 @@
             )
             val pagingSources = mutableListOf<TestPagingSource>()
             val pageFetcher = PageFetcher(
-                pagingSourceFactory = {
+                pagingSourceFactory = suspend {
                     TestPagingSource(loadDelay = 1000).also {
                         pagingSources.add(it)
                     }
@@ -582,7 +592,6 @@
     val pageEventLists: ArrayList<ArrayList<PageEvent<Int>>> = ArrayList()
 
     val job = launch {
-        @OptIn(ExperimentalCoroutinesApi::class)
         fetcher.flow.collectIndexed { index, pagingData ->
             pagingDataList.add(index, pagingData)
             pageEventLists.add(index, ArrayList())
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
index 44398e8..c94b1cf 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
@@ -18,6 +18,9 @@
 
 import androidx.testutils.TestDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -86,16 +89,17 @@
         }
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun loadFullVerify() {
         // validate paging entire ItemDataSource results in full, correctly ordered data
-        val testCoroutineScope = CoroutineScope(EmptyCoroutineContext)
-
+        val dispatcher = TestCoroutineDispatcher()
+        val testCoroutineScope = CoroutineScope(dispatcher)
         @Suppress("DEPRECATION")
         val pagedList = PagedList.Builder(ItemDataSource(), 100)
             .setCoroutineScope(testCoroutineScope)
-            .setNotifyDispatcher(mainThread)
-            .setFetchDispatcher(DirectDispatcher)
+            .setNotifyDispatcher(dispatcher)
+            .setFetchDispatcher(dispatcher)
             .build()
 
         // validate initial load
@@ -105,7 +109,7 @@
         for (i in 0..PAGE_MAP.keys.size) {
             pagedList.loadAround(0)
             pagedList.loadAround(pagedList.size - 1)
-            drain()
+            dispatcher.advanceUntilIdle()
         }
 
         // validate full load
@@ -148,7 +152,7 @@
         @Suppress("DEPRECATION")
         PagedList.Builder(dataSource, 10)
             .setNotifyDispatcher(FailDispatcher())
-            .setFetchDispatcher(DirectDispatcher)
+            .setFetchDispatcher(Dispatchers.IO)
             .build()
     }
 
@@ -248,7 +252,7 @@
         val pagedList = PagedList.Builder(dataSource, 10)
             .setBoundaryCallback(boundaryCallback)
             .setCoroutineScope(testCoroutineScope)
-            .setFetchDispatcher(dispatcher)
+            .setFetchDispatcher(Dispatchers.Unconfined)
             .setNotifyDispatcher(dispatcher)
             .build()
 
@@ -301,7 +305,7 @@
         val pagedList = PagedList.Builder(dataSource, 10)
             .setBoundaryCallback(boundaryCallback)
             .setCoroutineScope(testCoroutineScope)
-            .setFetchDispatcher(dispatcher)
+            .setFetchDispatcher(Dispatchers.Unconfined)
             .setNotifyDispatcher(dispatcher)
             .build()
 
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
index abb5242..a0ea57f 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
@@ -21,11 +21,14 @@
 import androidx.testutils.TestDispatcher
 import androidx.testutils.TestExecutor
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import java.util.concurrent.Executor
+import kotlin.concurrent.thread
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.test.assertFailsWith
 import kotlin.test.assertTrue
@@ -53,10 +56,18 @@
 
     @Test
     fun createLegacy() {
+        val slowFetchExecutor = Executor {
+            // just be slow to ensure `build()` really waited on fetch to complete.
+            // but still run it on another thread to ensure we are not blocking the test here
+            thread {
+                Thread.sleep(1000)
+                it.run()
+            }
+        }
         @Suppress("DEPRECATION")
         val pagedList = PagedList.Builder(TestPositionalDataSource(ITEMS), 100)
             .setNotifyExecutor(TestExecutor())
-            .setFetchExecutor(TestExecutor())
+            .setFetchExecutor(slowFetchExecutor)
             .build()
         // if build succeeds without flushing an executor, success!
         assertEquals(ITEMS, pagedList)
@@ -76,8 +87,8 @@
                     pagingSource,
                     null,
                     testCoroutineScope,
-                    DirectDispatcher,
-                    DirectDispatcher,
+                    Dispatchers.Default,
+                    Dispatchers.IO,
                     null,
                     Config(10),
                     0
@@ -104,8 +115,8 @@
                     pagingSource,
                     null,
                     testCoroutineScope,
-                    DirectDispatcher,
-                    DirectDispatcher,
+                    Dispatchers.Default,
+                    Dispatchers.IO,
                     null,
                     Config(10),
                     0
@@ -121,14 +132,13 @@
                 key = null,
                 loadSize = 10,
                 placeholdersEnabled = false,
-                pageSize = 10
             )
         ) as PagingSource.LoadResult.Page
 
         @Suppress("DEPRECATION")
         val pagedList = PagedList.Builder(pagingSource, initialPage, config)
-            .setNotifyDispatcher(DirectDispatcher)
-            .setFetchDispatcher(DirectDispatcher)
+            .setNotifyDispatcher(Dispatchers.Default)
+            .setFetchDispatcher(Dispatchers.IO)
             .build()
 
         assertEquals(pagingSource, pagedList.pagingSource)
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
index 6a6395c..883b4f2 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
@@ -524,8 +524,12 @@
         previousList: NullPaddedList<Int>,
         newList: NullPaddedList<Int>,
         newCombinedLoadStates: CombinedLoadStates,
-        lastAccessedIndex: Int
-    ): Int? = null
+        lastAccessedIndex: Int,
+        onListPresentable: () -> Unit
+    ): Int? {
+        onListPresentable()
+        return null
+    }
 }
 
 internal val dummyReceiver = object : UiReceiver {
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
index 54047f9..6f90f6b 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -43,7 +43,6 @@
                 key,
                 initialLoadSize,
                 enablePlaceholders,
-                10
             )
         )
     }
@@ -61,7 +60,7 @@
 
             // Verify error is propagated correctly.
             pagingSource.enqueueError()
-            val errorParams = LoadParams.Refresh(key, 10, false, 10)
+            val errorParams = LoadParams.Refresh(key, 10, false)
             assertFailsWith<CustomException> {
                 pagingSource.load(errorParams)
             }
@@ -193,7 +192,7 @@
 
         runBlocking {
             val key = ITEMS_BY_NAME_ID[5].key()
-            val params = LoadParams.Prepend(key, 5, false, 5)
+            val params = LoadParams.Prepend(key, 5, false)
             val observed = (dataSource.load(params) as LoadResult.Page).data
 
             assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed)
@@ -201,7 +200,7 @@
             // Verify error is propagated correctly.
             dataSource.enqueueError()
             assertFailsWith<CustomException> {
-                val errorParams = LoadParams.Prepend(key, 5, false, 5)
+                val errorParams = LoadParams.Prepend(key, 5, false)
                 dataSource.load(errorParams)
             }
         }
@@ -213,7 +212,7 @@
 
         runBlocking {
             val key = ITEMS_BY_NAME_ID[5].key()
-            val params = LoadParams.Append(key, 5, false, 5)
+            val params = LoadParams.Append(key, 5, false)
             val observed = (dataSource.load(params) as LoadResult.Page).data
 
             assertEquals(ITEMS_BY_NAME_ID.subList(6, 11), observed)
@@ -221,7 +220,7 @@
             // Verify error is propagated correctly.
             dataSource.enqueueError()
             assertFailsWith<CustomException> {
-                val errorParams = LoadParams.Append(key, 5, false, 5)
+                val errorParams = LoadParams.Append(key, 5, false)
                 dataSource.load(errorParams)
             }
         }
@@ -257,7 +256,6 @@
 
         private var error = false
 
-        @OptIn(ExperimentalPagingApi::class)
         override fun getRefreshKey(state: PagingState<Key, Item>): Key? {
             return state.anchorPosition
                 ?.let { anchorPosition -> state.closestItemToPosition(anchorPosition) }
diff --git a/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt b/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
index 151a55db..29d1dbd 100644
--- a/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
@@ -39,6 +39,7 @@
 class RemoteMediatorAccessorTest {
     private val testScope = TestCoroutineScope()
     private var mockStateId = 0
+
     // creates a unique state using the anchor position to be able to do equals check in assertions
     private fun createMockState(
         anchorPosition: Int? = mockStateId++
@@ -52,6 +53,110 @@
     }
 
     @Test
+    fun load_reportsPrependLoadState() = testScope.runBlockingTest {
+        val emptyState = PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED)
+        val remoteMediator = RemoteMediatorMock(loadDelay = 1000)
+        val remoteMediatorAccessor = createAccessor(remoteMediator)
+
+        // Assert initial state is NotLoading.Incomplete.
+        assertEquals(
+            LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete),
+            remoteMediatorAccessor.state.value,
+        )
+
+        // Start a PREPEND load.
+        remoteMediatorAccessor.requestLoad(
+            loadType = PREPEND,
+            pagingState = emptyState,
+        )
+
+        // Assert state is immediately set to Loading.
+        assertEquals(
+            LoadStates.IDLE.copy(prepend = LoadState.Loading),
+            remoteMediatorAccessor.state.value,
+        )
+
+        // Wait for load to finish.
+        advanceUntilIdle()
+
+        // Assert state is set to NotLoading.Incomplete.
+        assertEquals(
+            LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete),
+            remoteMediatorAccessor.state.value,
+        )
+
+        // Start a PREPEND load which results in endOfPaginationReached = true.
+        remoteMediator.loadCallback = { _, _ ->
+            RemoteMediator.MediatorResult.Success(endOfPaginationReached = true)
+        }
+        remoteMediatorAccessor.requestLoad(
+            loadType = PREPEND,
+            pagingState = emptyState,
+        )
+
+        // Wait for load to finish.
+        advanceUntilIdle()
+
+        // Assert state is set to NotLoading.Incomplete.
+        assertEquals(
+            LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Complete),
+            remoteMediatorAccessor.state.value,
+        )
+    }
+
+    @Test
+    fun load_reportsAppendLoadState() = testScope.runBlockingTest {
+        val emptyState = PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED)
+        val remoteMediator = RemoteMediatorMock(loadDelay = 1000)
+        val remoteMediatorAccessor = createAccessor(remoteMediator)
+
+        // Assert initial state is NotLoading.Incomplete.
+        assertEquals(
+            LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete),
+            remoteMediatorAccessor.state.value,
+        )
+
+        // Start a APPEND load.
+        remoteMediatorAccessor.requestLoad(
+            loadType = APPEND,
+            pagingState = emptyState,
+        )
+
+        // Assert state is immediately set to Loading.
+        assertEquals(
+            LoadStates.IDLE.copy(append = LoadState.Loading),
+            remoteMediatorAccessor.state.value,
+        )
+
+        // Wait for load to finish.
+        advanceUntilIdle()
+
+        // Assert state is set to NotLoading.Incomplete.
+        assertEquals(
+            LoadStates.IDLE.copy(append = LoadState.NotLoading.Incomplete),
+            remoteMediatorAccessor.state.value,
+        )
+
+        // Start a APPEND load which results in endOfPaginationReached = true.
+        remoteMediator.loadCallback = { _, _ ->
+            RemoteMediator.MediatorResult.Success(endOfPaginationReached = true)
+        }
+        remoteMediatorAccessor.requestLoad(
+            loadType = APPEND,
+            pagingState = emptyState,
+        )
+
+        // Wait for load to finish.
+        advanceUntilIdle()
+
+        // Assert state is set to NotLoading.Incomplete.
+        assertEquals(
+            LoadStates.IDLE.copy(append = LoadState.NotLoading.Complete),
+            remoteMediatorAccessor.state.value,
+        )
+    }
+
+    @Test
     fun load_conflatesPrepend() = testScope.runBlockingTest {
         val remoteMediator = RemoteMediatorMock(loadDelay = 1000)
         val remoteMediatorAccessor = createAccessor(remoteMediator)
diff --git a/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
index 38d8dc7..276285b 100644
--- a/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
@@ -19,10 +19,12 @@
 import androidx.paging.LoadState.NotLoading
 import androidx.paging.LoadType.APPEND
 import androidx.paging.LoadType.PREPEND
+import androidx.paging.LoadType.REFRESH
 import androidx.paging.PageEvent.Drop
 import androidx.paging.PageEvent.Insert.Companion.Append
 import androidx.paging.PageEvent.Insert.Companion.Prepend
 import androidx.paging.PageEvent.Insert.Companion.Refresh
+import androidx.paging.PageEvent.LoadStateUpdate
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
@@ -985,6 +987,481 @@
         )
     }
 
+    @Test
+    fun remoteRefreshEndOfPaginationReached() = runBlockingTest {
+        assertThat(
+            flowOf(
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = LoadState.Loading
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = false,
+                    loadState = LoadState.Loading
+                ),
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                LoadStateUpdate(
+                    loadType = PREPEND,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                LoadStateUpdate(
+                    loadType = APPEND,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+        ).isEqualTo(
+            listOf(
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = LoadState.Loading
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = false,
+                    loadState = LoadState.Loading
+                ),
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersBefore = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        refresh = NotLoading.Complete,
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        refreshRemote = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    ),
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        refresh = NotLoading.Complete,
+                        prepend = NotLoading.Complete,
+                        append = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        refreshRemote = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    ),
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remotePrependEndOfPaginationReached() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote prepend is done triggers the header to resolve.
+                LoadStateUpdate(PREPEND, true, NotLoading.Complete),
+            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersBefore = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    ),
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remotePrependEndOfPaginationReachedWithDrops() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("b1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Incomplete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        )
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote prepend is done triggers the header to resolve.
+                LoadStateUpdate(PREPEND, true, NotLoading.Complete),
+                // Drop the first page, header and separator between "b1" and "a1"
+                Drop(
+                    loadType = PREPEND,
+                    minPageOffset = -1,
+                    maxPageOffset = -1,
+                    placeholdersRemaining = 1
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        )
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    )
+                ),
+            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(listOf("b1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Incomplete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1, 0),
+                            data = listOf("B"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1
+                        ),
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1,
+                        ),
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    ),
+                ),
+                Drop(
+                    loadType = PREPEND,
+                    minPageOffset = -1,
+                    maxPageOffset = -1,
+                    placeholdersRemaining = 1
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1,
+                        ),
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1, 0),
+                            data = listOf("B"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1
+                        ),
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    )
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remoteAppendEndOfPaginationReached() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote append is done triggers the footer to resolve.
+                LoadStateUpdate(APPEND, true, NotLoading.Complete),
+            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    ),
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remoteAppendEndOfPaginationReachedWithDrops() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("b1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Incomplete,
+                    )
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        )
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote append is done triggers the footer to resolve.
+                LoadStateUpdate(APPEND, true, NotLoading.Complete),
+                // Drop the last page, footer and separator between "b1" and "c1"
+                Drop(
+                    loadType = APPEND,
+                    minPageOffset = 1,
+                    maxPageOffset = 1,
+                    placeholdersRemaining = 1
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        )
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    )
+                ),
+            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(listOf("b1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Incomplete,
+                    )
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0, 1),
+                            data = listOf("C"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        ),
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                    )
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(1),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1,
+                        ),
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    ),
+                ),
+                Drop(
+                    loadType = APPEND,
+                    minPageOffset = 1,
+                    maxPageOffset = 1,
+                    placeholdersRemaining = 1
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0, 1),
+                            data = listOf("C"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(1),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1
+                        ),
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    )
+                ),
+            )
+        )
+    }
+
     companion object {
         /**
          * Creates an upper-case letter at the beginning of each section of strings that start
diff --git a/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
index 2c90329..f642d6c 100644
--- a/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
@@ -21,7 +21,6 @@
 import org.junit.runners.JUnit4
 import kotlin.test.assertTrue
 
-@OptIn(ExperimentalPagingApi::class)
 @RunWith(JUnit4::class)
 class WrappedItemKeyedDataSourceTest {
 
diff --git a/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
index 129cd3f..cf4ef24 100644
--- a/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
@@ -21,7 +21,6 @@
 import org.junit.runners.JUnit4
 import kotlin.test.assertTrue
 
-@OptIn(ExperimentalPagingApi::class)
 @RunWith(JUnit4::class)
 class WrappedPageKeyedDataSourceTest {
 
diff --git a/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
index cc02f84..b56fe139 100644
--- a/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
@@ -21,7 +21,6 @@
 import org.junit.runners.JUnit4
 import kotlin.test.assertTrue
 
-@OptIn(ExperimentalPagingApi::class)
 @RunWith(JUnit4::class)
 class WrappedPositionalDataSourceTest {
 
diff --git a/paging/guava/src/test/java/androidx/paging/ListenableFuturePagingSourceTest.kt b/paging/guava/src/test/java/androidx/paging/ListenableFuturePagingSourceTest.kt
index 811c92a..1b4ef95 100644
--- a/paging/guava/src/test/java/androidx/paging/ListenableFuturePagingSourceTest.kt
+++ b/paging/guava/src/test/java/androidx/paging/ListenableFuturePagingSourceTest.kt
@@ -60,14 +60,14 @@
 
     @Test
     fun basic() = runBlocking {
-        val params = LoadParams.Refresh(0, 2, false, 2)
+        val params = LoadParams.Refresh(0, 2, false)
         assertEquals(pagingSource.load(params), listenableFuturePagingSource.load(params))
     }
 
     @Test
     fun error() {
         runBlocking {
-            val params = LoadParams.Refresh<Int>(null, 2, false, 2)
+            val params = LoadParams.Refresh<Int>(null, 2, false)
             assertFailsWith<NullPointerException> { pagingSource.load(params) }
             assertFailsWith<NullPointerException> { listenableFuturePagingSource.load(params) }
         }
@@ -76,7 +76,7 @@
     @Test
     fun errorWrapped() {
         runBlocking {
-            val params = LoadParams.Refresh(-1, 2, false, 2)
+            val params = LoadParams.Refresh(-1, 2, false)
             assertFailsWith<IllegalArgumentException> { pagingSource.load(params) }
             assertFailsWith<IllegalArgumentException> { listenableFuturePagingSource.load(params) }
         }
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt
index 5ce84fd..a107d6e 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt
@@ -18,7 +18,6 @@
 
 import android.graphics.Color
 import androidx.annotation.ColorInt
-import androidx.paging.ExperimentalPagingApi
 import androidx.paging.PagingSource
 import androidx.paging.PagingState
 import kotlinx.coroutines.delay
@@ -34,7 +33,6 @@
 
     private val generationId = sGenerationId++
 
-    @OptIn(ExperimentalPagingApi::class)
     override fun getRefreshKey(state: PagingState<Int, Item>): Int? = state.anchorPosition
 
     override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> =
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java
index 1b400ca..58cd34c 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java
@@ -52,16 +52,16 @@
     void removeAll();
 
     /**
-     * @return DataSource.Factory of customers, ordered by last name. Use
+     * @return DataSource.Factory of customers, ordered by id. Use
      * {@link androidx.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
      */
-    @Query("SELECT * FROM customer ORDER BY mLastName ASC")
+    @Query("SELECT * FROM customer ORDER BY mId ASC")
     DataSource.Factory<Integer, Customer> loadPagedAgeOrder();
 
     /**
-     * @return PagingSource of customers, ordered by last name.
+     * @return PagingSource of customers, ordered by id.
      */
-    @Query("SELECT * FROM customer ORDER BY mLastName ASC")
+    @Query("SELECT * FROM customer ORDER BY mId ASC")
     PagingSource<Integer, Customer> loadPagedAgeOrderPagingSource();
 
     /**
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt
index a8eb290..e7f4e4a 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt
@@ -18,7 +18,6 @@
 
 import android.graphics.Color
 import androidx.annotation.ColorInt
-import androidx.paging.ExperimentalPagingApi
 import androidx.paging.PagingSource
 import androidx.paging.PagingState
 import kotlinx.coroutines.delay
@@ -34,7 +33,6 @@
 
     private val generationId = sGenerationId++
 
-    @OptIn(ExperimentalPagingApi::class)
     override fun getRefreshKey(state: PagingState<Int, Item>): Int? = state.anchorPosition
 
     override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> =
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt
index bfd595f..a8288e4 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt
@@ -30,7 +30,6 @@
 import androidx.paging.integration.testapp.R
 import androidx.paging.map
 import androidx.recyclerview.widget.RecyclerView
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
@@ -50,7 +49,6 @@
         }
         // NOTE: lifecycleScope means we don't respect paused state here
         lifecycleScope.launch {
-            @OptIn(ExperimentalCoroutinesApi::class)
             viewModel.flow
                 .map { pagingData ->
                     pagingData.map { it.copy(text = "${it.text} - $orientationText") }
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt
index ae1744b..13709de 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt
@@ -16,7 +16,6 @@
 
 package androidx.paging.integration.testapp.v3room
 
-import androidx.paging.ExperimentalPagingApi
 import androidx.paging.PagingSource
 import androidx.paging.PagingState
 import androidx.paging.integration.testapp.room.Customer
@@ -28,20 +27,27 @@
 internal class NetworkCustomerPagingSource : PagingSource<Int, Customer>() {
     private fun createCustomer(i: Int): Customer {
         val customer = Customer()
-        customer.name = UUID.randomUUID().toString()
+        customer.name = "customer_$i"
         customer.lastName = "${"%04d".format(i)}_${UUID.randomUUID()}"
         return customer
     }
 
-    @OptIn(ExperimentalPagingApi::class)
-    override fun getRefreshKey(state: PagingState<Int, Customer>): Int? = state.anchorPosition
+    override fun getRefreshKey(
+        state: PagingState<Int, Customer>
+    ): Int? = state.anchorPosition?.let {
+        maxOf(0, it - 5)
+    }
 
     override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Customer> {
         val key = params.key ?: 0
-        val data = List(params.loadSize) { createCustomer(it + key) }
+        val data = if (params is LoadParams.Prepend) {
+            List(params.loadSize) { createCustomer(it + key - params.loadSize) }
+        } else {
+            List(params.loadSize) { createCustomer(it + key) }
+        }
         return LoadResult.Page(
             data = data,
-            prevKey = if (key > 0) key - 1 else null,
+            prevKey = if (key > 0) key else null,
             nextKey = key + data.size
         )
     }
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt
index de5eaf8..761a3d8 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt
@@ -36,7 +36,9 @@
         loadType: LoadType,
         state: PagingState<Int, Customer>
     ): MediatorResult {
-        if (loadType == LoadType.PREPEND) return MediatorResult.Success(false)
+        if (loadType == LoadType.PREPEND) {
+            return MediatorResult.Success(endOfPaginationReached = true)
+        }
 
         // TODO: Move this to be a more fully featured sample which demonstrated key translation
         //  between two types of PagingSources where the keys do not map 1:1.
@@ -44,15 +46,13 @@
             LoadType.REFRESH -> PagingSource.LoadParams.Refresh(
                 key = 0,
                 loadSize = 10,
-                placeholdersEnabled = false,
-                pageSize = 10
+                placeholdersEnabled = false
             )
             LoadType.PREPEND -> throw IllegalStateException()
             LoadType.APPEND -> PagingSource.LoadParams.Append(
                 key = state.pages.lastOrNull()?.nextKey ?: 0,
                 loadSize = 10,
-                placeholdersEnabled = false,
-                pageSize = 10
+                placeholdersEnabled = false
             )
         }
 
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt
index 454b2d2..8bad4dc 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt
@@ -23,7 +23,6 @@
 import androidx.lifecycle.lifecycleScope
 import androidx.paging.integration.testapp.R
 import androidx.recyclerview.widget.RecyclerView
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 
@@ -39,7 +38,6 @@
         recyclerView.adapter = adapter
 
         lifecycleScope.launch {
-            @OptIn(ExperimentalCoroutinesApi::class)
             viewModel.flow.collectLatest {
                 adapter.submitData(it)
             }
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt
index 5e0f1bf..a1e2dfb 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt
@@ -40,7 +40,7 @@
     override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
         val item = getItem(position)
         if (item != null) {
-            (holder.itemView as TextView).text = item.lastName
+            (holder.itemView as TextView).text = item.name
             holder.itemView.setBackgroundColor(Color.BLUE)
         } else {
             (holder.itemView as TextView).setText(R.string.loading)
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
index 27e4eea..8c84a91 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
@@ -21,6 +21,7 @@
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.viewModelScope
+import androidx.paging.ExperimentalPagingApi
 import androidx.paging.Pager
 import androidx.paging.PagingConfig
 import androidx.paging.cachedIn
@@ -57,6 +58,7 @@
             .executeOnDiskIO { database.customerDao.removeAll() }
     }
 
+    @OptIn(ExperimentalPagingApi::class)
     val flow = Pager(
         PagingConfig(10),
         remoteMediator = V3RemoteMediator(
@@ -76,7 +78,7 @@
                         Customer().apply {
                             id = -1
                             name = "RIGHT ABOVE DIVIDER"
-                            lastName = "LAST NAME"
+                            lastName = "RIGHT ABOVE DIVIDER"
                         }
                     }
                 }
@@ -85,19 +87,21 @@
                         Customer().apply {
                             id = -2
                             name = "RIGHT BELOW DIVIDER"
-                            lastName = "LAST NAME"
+                            lastName = "RIGHT BELOW DIVIDER"
                         }
                     } else null
                 }
                 .insertHeaderItem(
                     Customer().apply {
                         id = Int.MIN_VALUE
+                        name = "HEADER"
                         lastName = "HEADER"
                     }
                 )
                 .insertFooterItem(
                     Customer().apply {
                         id = Int.MAX_VALUE
+                        name = "FOOTER"
                         lastName = "FOOTER"
                     }
                 )
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 2d099c2..ef5d031 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -35,7 +35,7 @@
 
     implementation(KOTLIN_STDLIB)
     api projectOrArtifact(":compose:foundation:foundation")
-    api("androidx.paging:paging-common-ktx:3.0.0-alpha06")
+    api projectOrArtifact(":paging:paging-common")
 
     androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
     androidTestImplementation projectOrArtifact(':internal-testutils-paging')
diff --git a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
index 12200c8..f6b3ac0 100644
--- a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
+++ b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
@@ -89,8 +89,10 @@
             previousList: NullPaddedList<T>,
             newList: NullPaddedList<T>,
             newCombinedLoadStates: CombinedLoadStates,
-            lastAccessedIndex: Int
+            lastAccessedIndex: Int,
+            onListPresentable: () -> Unit,
         ): Int? {
+            onListPresentable()
             // TODO: This logic may be changed after the implementation of an async model which
             //  composes the offscreen elements
             recomposerPlaceholder.value++
@@ -166,7 +168,12 @@
      * A [CombinedLoadStates] object which represents the current loading state.
      */
     public var loadState: CombinedLoadStates by mutableStateOf(
-        CombinedLoadStates(InitialLoadStates)
+        CombinedLoadStates(
+            refresh = InitialLoadStates.refresh,
+            prepend = InitialLoadStates.prepend,
+            append = InitialLoadStates.append,
+            source = InitialLoadStates,
+        )
     )
         private set
 
diff --git a/paging/runtime/build.gradle b/paging/runtime/build.gradle
index 9a80921..4c3019b 100644
--- a/paging/runtime/build.gradle
+++ b/paging/runtime/build.gradle
@@ -26,6 +26,12 @@
     id("kotlin-android")
 }
 
+android {
+    defaultConfig {
+        multiDexEnabled true
+    }
+}
+
 dependencies {
     api(project(":paging:paging-common"))
     // Ensure that the -ktx dependency graph mirrors the Java dependency graph
@@ -46,9 +52,11 @@
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation("androidx.arch.core:core-testing:2.1.0")
+    androidTestImplementation(TRUTH)
     androidTestImplementation(KOTLIN_TEST)
     androidTestImplementation(KOTLIN_COROUTINES_TEST)
     androidTestImplementation(JUNIT)
+    androidTestImplementation(MULTIDEX)
 }
 
 androidx {
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
index 0435993..b0e3005 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
@@ -65,11 +65,16 @@
         data: List<V>,
         initialKey: Int
     ): PagedList<V> {
+        // unblock page loading thread to allow build to succeed
+        pageLoadingThread.autoRun = true
         return PagedList.Builder(TestPositionalDataSource(data), config)
             .setInitialKey(initialKey)
             .setNotifyExecutor(mainThread)
             .setFetchExecutor(pageLoadingThread)
             .build()
+            .also {
+                pageLoadingThread.autoRun = false
+            }
     }
 
     @Test
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
index 6bb8c66..6f89fd1 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
@@ -23,7 +23,6 @@
 import androidx.paging.ListUpdateEvent.Removed
 import androidx.paging.LoadState.Loading
 import androidx.paging.LoadState.NotLoading
-import androidx.paging.LoadType.REFRESH
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.ListUpdateCallback
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -133,9 +132,10 @@
         // empty previous list.
         assertEvents(
             listOf(
-                REFRESH to Loading,
-                REFRESH to NotLoading(endOfPaginationReached = false)
-            ).toCombinedLoadStatesLocal(),
+                localLoadStatesOf(),
+                localLoadStatesOf(refreshLocal = Loading),
+                localLoadStatesOf(refreshLocal = NotLoading(endOfPaginationReached = false)),
+            ),
             loadEvents
         )
         loadEvents.clear()
@@ -152,8 +152,8 @@
                 localLoadStatesOf(
                     refreshLocal = NotLoading(endOfPaginationReached = false),
                     prependLocal = NotLoading(endOfPaginationReached = true),
-                    appendLocal = NotLoading(endOfPaginationReached = true)
-                )
+                    appendLocal = NotLoading(endOfPaginationReached = true),
+                ),
             ),
             actual = loadEvents
         )
@@ -190,9 +190,10 @@
         // empty previous list.
         assertEvents(
             listOf(
-                REFRESH to Loading,
-                REFRESH to NotLoading(endOfPaginationReached = false)
-            ).toCombinedLoadStatesLocal(),
+                localLoadStatesOf(),
+                localLoadStatesOf(refreshLocal = Loading),
+                localLoadStatesOf(refreshLocal = NotLoading(endOfPaginationReached = false)),
+            ),
             loadEvents
         )
         loadEvents.clear()
@@ -209,8 +210,8 @@
                 localLoadStatesOf(
                     refreshLocal = NotLoading(endOfPaginationReached = false),
                     prependLocal = NotLoading(endOfPaginationReached = true),
-                    appendLocal = NotLoading(endOfPaginationReached = true)
-                )
+                    appendLocal = NotLoading(endOfPaginationReached = true),
+                ),
             ),
             actual = loadEvents
         )
@@ -510,48 +511,21 @@
 
         // Initial refresh
         advanceUntilIdle()
-        assertEquals(
-            CombinedLoadStates(
-                source = LoadStates(
-                    refresh = NotLoading(endOfPaginationReached = false),
-                    prepend = NotLoading(endOfPaginationReached = false),
-                    append = NotLoading(endOfPaginationReached = false)
-                )
-            ),
-            combinedLoadStates
-        )
+        assertEquals(localLoadStatesOf(), combinedLoadStates)
         assertEquals(10, itemCount)
         assertEquals(10, differ.itemCount)
 
         // Append
         differ.getItem(9)
         advanceUntilIdle()
-        assertEquals(
-            CombinedLoadStates(
-                source = LoadStates(
-                    refresh = NotLoading(endOfPaginationReached = false),
-                    prepend = NotLoading(endOfPaginationReached = false),
-                    append = NotLoading(endOfPaginationReached = false)
-                )
-            ),
-            combinedLoadStates
-        )
+        assertEquals(localLoadStatesOf(), combinedLoadStates)
         assertEquals(20, itemCount)
         assertEquals(20, differ.itemCount)
 
         // Prepend
         differ.getItem(0)
         advanceUntilIdle()
-        assertEquals(
-            CombinedLoadStates(
-                source = LoadStates(
-                    refresh = NotLoading(endOfPaginationReached = false),
-                    prepend = NotLoading(endOfPaginationReached = false),
-                    append = NotLoading(endOfPaginationReached = false)
-                )
-            ),
-            combinedLoadStates
-        )
+        assertEquals(localLoadStatesOf(), combinedLoadStates)
         assertEquals(30, itemCount)
         assertEquals(30, differ.itemCount)
 
@@ -584,52 +558,90 @@
 
             // Initial refresh
             advanceUntilIdle()
-            assertEquals(
-                CombinedLoadStates(
-                    source = LoadStates(
-                        refresh = NotLoading(endOfPaginationReached = false),
-                        prepend = NotLoading(endOfPaginationReached = false),
-                        append = NotLoading(endOfPaginationReached = false)
-                    )
-                ),
-                combinedLoadStates
-            )
+            assertEquals(localLoadStatesOf(), combinedLoadStates)
             assertEquals(10, itemCount)
             assertEquals(10, differ.itemCount)
 
             // Append
             differ.getItem(9)
             advanceUntilIdle()
-            assertEquals(
-                CombinedLoadStates(
-                    source = LoadStates(
-                        refresh = NotLoading(endOfPaginationReached = false),
-                        prepend = NotLoading(endOfPaginationReached = false),
-                        append = NotLoading(endOfPaginationReached = false)
-                    )
-                ),
-                combinedLoadStates
-            )
+            assertEquals(localLoadStatesOf(), combinedLoadStates)
             assertEquals(20, itemCount)
             assertEquals(20, differ.itemCount)
 
             // Prepend
             differ.getItem(0)
             advanceUntilIdle()
-            assertEquals(
-                CombinedLoadStates(
-                    source = LoadStates(
-                        refresh = NotLoading(endOfPaginationReached = false),
-                        prepend = NotLoading(endOfPaginationReached = false),
-                        append = NotLoading(endOfPaginationReached = false)
-                    )
-                ),
-                combinedLoadStates
-            )
+            assertEquals(localLoadStatesOf(), combinedLoadStates)
             assertEquals(30, itemCount)
             assertEquals(30, differ.itemCount)
 
             job.cancel()
         }
     }
+
+    @Test
+    fun listUpdateCallbackSynchronouslyUpdates() = testScope.runBlockingTest {
+        pauseDispatcher {
+            // Keep track of .snapshot() result within each ListUpdateCallback
+            val initialSnapshot: ItemSnapshotList<Int> = ItemSnapshotList(0, 0, emptyList())
+            var onInsertedSnapshot = initialSnapshot
+            var onRemovedSnapshot = initialSnapshot
+
+            val listUpdateCallback = object : ListUpdateCallback {
+                lateinit var differ: AsyncPagingDataDiffer<Int>
+
+                override fun onChanged(position: Int, count: Int, payload: Any?) {
+                    // TODO: Trigger this callback so we can assert state at this point as well
+                }
+
+                override fun onMoved(fromPosition: Int, toPosition: Int) {
+                    // TODO: Trigger this callback so we can assert state at this point as well
+                }
+
+                override fun onInserted(position: Int, count: Int) {
+                    onInsertedSnapshot = differ.snapshot()
+                }
+
+                override fun onRemoved(position: Int, count: Int) {
+                    onRemovedSnapshot = differ.snapshot()
+                }
+            }
+
+            val differ = AsyncPagingDataDiffer(
+                diffCallback = object : DiffUtil.ItemCallback<Int>() {
+                    override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+
+                    override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+                },
+                updateCallback = listUpdateCallback,
+                mainDispatcher = Dispatchers.Main,
+                workerDispatcher = Dispatchers.Main,
+            ).also {
+                listUpdateCallback.differ = it
+            }
+
+            // Initial insert; this only triggers onInserted
+            differ.submitData(PagingData.from(listOf(0)))
+            advanceUntilIdle()
+
+            val firstList = ItemSnapshotList(0, 0, listOf(0))
+            assertEquals(firstList, differ.snapshot())
+            assertEquals(firstList, onInsertedSnapshot)
+            assertEquals(initialSnapshot, onRemovedSnapshot)
+
+            // Switch item to 1; this triggers onInserted + onRemoved
+            differ.submitData(PagingData.from(listOf(1)))
+            advanceUntilIdle()
+
+            val secondList = ItemSnapshotList(0, 0, listOf(1))
+            assertEquals(secondList, differ.snapshot())
+            assertEquals(secondList, onInsertedSnapshot)
+            assertEquals(secondList, onRemovedSnapshot)
+        }
+    }
 }
\ No newline at end of file
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
index 6d27ee0..e2055a6 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
@@ -96,8 +96,11 @@
             }
 
             private fun loadInitial(params: LoadParams<Int>): LoadResult<Int, String> {
-                @Suppress("DEPRECATION")
-                assertEquals(2, params.pageSize)
+                if (params is LoadParams.Refresh) {
+                    assertEquals(6, params.loadSize)
+                } else {
+                    assertEquals(2, params.loadSize)
+                }
 
                 throwable?.let { error ->
                     throwable = null
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/StateRestorationTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/StateRestorationTest.kt
new file mode 100644
index 0000000..f7df7ee
--- /dev/null
+++ b/paging/runtime/src/androidTest/java/androidx/paging/StateRestorationTest.kt
@@ -0,0 +1,530 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging
+
+import android.app.Application
+import android.content.Context
+import android.os.Parcelable
+import android.view.View
+import android.view.View.MeasureSpec.EXACTLY
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW
+import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.internal.ThreadSafeHeap
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.time.ExperimentalTime
+
+/**
+ * We are only capable of restoring state if one the two is valid:
+ * a) pager's flow is cached in the view model (only for config change)
+ * b) data source is counted and placeholders are enabled (both config change and app restart)
+ *
+ * Both of these cases actually work without using the initial key, except it is relatively
+ * slower in option B because we need to load all items from initial key to the required position.
+ *
+ * This test validates those two cases for now. For more complicated cases, we need some helper
+ * as developer needs to intervene to provide more information.
+ */
+@ExperimentalCoroutinesApi
+@ExperimentalTime
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class StateRestorationTest {
+    /**
+     * List of dispatchers we track in the test for idling + pushing execution.
+     * We have 3 dispatchers for more granular control:
+     * main, and background for pager.
+     * testScope for running tests.
+     */
+    private val trackedDispatchers = mutableListOf<TestCoroutineDispatcher>()
+
+    private val mainDispatcher = TestCoroutineDispatcher().track()
+    private val backgroundDispatcher = TestCoroutineDispatcher().track()
+    private val testScope = TestCoroutineScope().track()
+
+    /**
+     * A fake lifecycle scope for collections that get cancelled when we recreate the recyclerview.
+     */
+    private lateinit var lifecycleScope: TestCoroutineScope
+    private lateinit var recyclerView: TestRecyclerView
+    private lateinit var layoutManager: RestoreAwareLayoutManager
+    private lateinit var adapter: TestAdapter
+
+    /**
+     * tracks [this] dispatcher for idling control.
+     */
+    private fun TestCoroutineDispatcher.track() = apply {
+        trackedDispatchers.add(this)
+    }
+
+    /**
+     * tracks the dispatcher of this scope for idling control.
+     */
+    private fun TestCoroutineScope.track() = apply {
+        (this@track.coroutineContext[ContinuationInterceptor.Key] as TestCoroutineDispatcher)
+            .track()
+    }
+
+    @Before
+    fun init() {
+        createRecyclerView()
+    }
+
+    @Test
+    fun restoreState_withPlaceholders() {
+        runTest {
+            collectPagesAsync(
+                createPager(
+                    pageSize = 100,
+                    enablePlaceholders = true
+                ).flow
+            )
+            measureAndLayout()
+            val visible = recyclerView.captureSnapshot()
+            assertThat(visible).isNotEmpty()
+            scrollToPosition(50)
+            val expected = recyclerView.captureSnapshot()
+            saveAndRestore()
+            // make sure state is not restored before items are loaded
+            assertThat(
+                layoutManager.restoredState
+            ).isFalse()
+            backgroundDispatcher.pauseDispatcher()
+            collectPagesAsync(
+                createPager(
+                    pageSize = 10,
+                    enablePlaceholders = true
+                ).flow
+            )
+            measureAndLayout()
+            // background worker is blocked, still shouldn't restore state
+            assertThat(
+                layoutManager.restoredState
+            ).isFalse()
+            backgroundDispatcher.resumeDispatcher()
+            measureAndLayout()
+            assertThat(
+                layoutManager.restoredState
+            ).isTrue()
+            assertThat(
+                recyclerView.captureSnapshot()
+            ).containsExactlyElementsIn(
+                expected
+            )
+        }
+    }
+
+    @Test
+    fun restoreState_withoutPlaceholders_cachedIn() {
+        runTest {
+            val pager = createPager(
+                pageSize = 60,
+                enablePlaceholders = false
+            )
+            val cacheScope = TestCoroutineScope(Job()).track()
+            val cachedFlow = pager.flow.cachedIn(cacheScope)
+            collectPagesAsync(cachedFlow)
+            measureAndLayout()
+            // now scroll
+            scrollToPosition(50)
+            val snapshot = recyclerView.captureSnapshot()
+            saveAndRestore()
+            assertThat(
+                layoutManager.restoredState
+            ).isFalse()
+            collectPagesAsync(cachedFlow)
+            measureAndLayout()
+            assertThat(
+                layoutManager.restoredState
+            ).isTrue()
+            val restoredSnapshot = recyclerView.captureSnapshot()
+            assertThat(restoredSnapshot).containsExactlyElementsIn(snapshot)
+            cacheScope.cancel()
+        }
+    }
+
+    @Test
+    fun emptyNewPage_allowRestoration() {
+        // check that we don't block restoration indefinitely if new pager is empty.
+        runTest {
+            val pager = createPager(
+                pageSize = 60,
+                enablePlaceholders = true
+            )
+            collectPagesAsync(pager.flow)
+            measureAndLayout()
+            scrollToPosition(50)
+            saveAndRestore()
+            assertThat(
+                layoutManager.restoredState
+            ).isFalse()
+            val emptyPager = createPager(
+                pageSize = 10,
+                itemCount = 0,
+                enablePlaceholders = true
+            )
+            collectPagesAsync(emptyPager.flow)
+            measureAndLayout()
+            assertThat(
+                layoutManager.restoredState
+            ).isTrue()
+        }
+    }
+
+    @Test
+    fun userOverridesStateRestoration() {
+        runTest {
+            val pager = createPager(
+                pageSize = 40,
+                enablePlaceholders = true
+            )
+            collectPagesAsync(pager.flow)
+            measureAndLayout()
+            scrollToPosition(20)
+            val snapshot = recyclerView.captureSnapshot()
+            saveAndRestore()
+            val pager2 = createPager(
+                pageSize = 40,
+                enablePlaceholders = true
+            )
+            // when user calls prevent, we should not trigger state restoration even after we
+            // receive the first page
+            adapter.stateRestorationPolicy = PREVENT
+            collectPagesAsync(pager2.flow)
+            measureAndLayout()
+            assertThat(
+                layoutManager.restoredState
+            ).isFalse()
+            // make sure test did work as expected, that is, new items are loaded
+            assertThat(adapter.itemCount).isGreaterThan(0)
+            // now if user allows it, restoration should happen properly
+            adapter.stateRestorationPolicy = ALLOW
+            measureAndLayout()
+            assertThat(
+                layoutManager.restoredState
+            ).isTrue()
+            assertThat(recyclerView.captureSnapshot()).isEqualTo(snapshot)
+        }
+    }
+
+    private fun createRecyclerView() {
+        // cancel previous lifecycle if it exists
+        if (this::lifecycleScope.isInitialized) {
+            this.lifecycleScope.cancel()
+        }
+        lifecycleScope = TestCoroutineScope(
+            SupervisorJob() + mainDispatcher
+        ).track()
+        val context = ApplicationProvider.getApplicationContext<Application>()
+        recyclerView = TestRecyclerView(context)
+        recyclerView.itemAnimator = null
+        adapter = TestAdapter()
+        recyclerView.adapter = adapter
+        layoutManager = RestoreAwareLayoutManager(context)
+        recyclerView.layoutManager = layoutManager
+    }
+
+    private fun runPending() {
+        while (trackedDispatchers.any { it.isNotEmpty && it.isNotPaused }) {
+            trackedDispatchers.filter { it.isNotPaused }.forEach {
+                it.runCurrent()
+            }
+        }
+    }
+
+    private fun scrollToPosition(pos: Int) {
+        while (adapter.itemCount <= pos) {
+            val prevSize = adapter.itemCount
+            adapter.triggerItemLoad(prevSize - 1)
+            runPending()
+            // this might be an issue with dropping but it is not the case here
+            assertWithMessage("more items should be loaded")
+                .that(adapter.itemCount)
+                .isGreaterThan(prevSize)
+        }
+        runPending()
+        recyclerView.scrollToPosition(pos)
+        measureAndLayout()
+        val child = layoutManager.findViewByPosition(pos)
+        assertWithMessage("scrolled child $pos exists")
+            .that(child)
+            .isNotNull()
+
+        val vh = recyclerView.getChildViewHolder(child!!) as ItemViewHolder
+        assertWithMessage("scrolled child should be fully loaded")
+            .that(vh.item)
+            .isNotNull()
+    }
+
+    private fun measureAndLayout() {
+        runPending()
+        while (recyclerView.isLayoutRequested) {
+            measure()
+            layout()
+            runPending()
+        }
+    }
+
+    private fun measure() {
+        recyclerView.measure(EXACTLY or RV_WIDTH, EXACTLY or RV_HEIGHT)
+    }
+
+    private fun layout() {
+        recyclerView.layout(0, 0, 100, 200)
+    }
+
+    private fun saveAndRestore() {
+        val state = recyclerView.saveState()
+        createRecyclerView()
+        recyclerView.restoreState(state)
+        measureAndLayout()
+    }
+
+    private fun runTest(block: TestCoroutineScope.() -> Unit) {
+        testScope.runBlockingTest {
+            try {
+                this.block()
+            } finally {
+                runPending()
+                // always cancel the lifecycle scope to ensure any collection there ends
+                if (this@StateRestorationTest::lifecycleScope.isInitialized) {
+                    lifecycleScope.cancel()
+                }
+            }
+        }
+    }
+
+    /**
+     * collects pages in the lifecycle scope and sends them to the adapter
+     */
+    private fun collectPagesAsync(
+        flow: Flow<PagingData<Item>>
+    ) {
+        val targetAdapter = adapter
+        lifecycleScope.launch {
+            flow.collectLatest {
+                targetAdapter.submitData(it)
+            }
+        }
+    }
+
+    private fun createPager(
+        pageSize: Int,
+        enablePlaceholders: Boolean,
+        itemCount: Int = 100,
+        initialKey: Int? = null
+    ): Pager<Int, Item> {
+        return Pager(
+            config = PagingConfig(
+                pageSize = pageSize,
+                enablePlaceholders = enablePlaceholders,
+            ),
+            initialKey = initialKey,
+            pagingSourceFactory = {
+                ItemPagingSource(
+                    context = backgroundDispatcher,
+                    items = (0 until itemCount).map { Item(it) }
+                )
+            }
+        )
+    }
+
+    /**
+     * Returns the list of all visible items in the recyclerview including their locations.
+     */
+    private fun RecyclerView.captureSnapshot(): List<PositionSnapshot> {
+        return (0 until childCount).mapNotNull {
+            val child = getChildAt(it)
+            // if child is not visible, ignore it as RV might have extra views around the visible
+            // area.
+            if (child.top >= height || child.bottom <= 0) {
+                // not visible, ignore
+                null
+            } else {
+                val vh = getChildViewHolder(child)
+                (vh as ItemViewHolder).captureSnapshot()
+            }
+        }
+    }
+
+    class ItemView(context: Context) : View(context)
+
+    class ItemViewHolder(context: Context) : RecyclerView.ViewHolder(ItemView(context)) {
+        var item: Item? = null
+            private set
+
+        init {
+            itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
+        }
+
+        fun captureSnapshot(): PositionSnapshot {
+            val item = checkNotNull(item)
+            return PositionSnapshot(
+                item = item,
+                top = itemView.top,
+                bottom = itemView.bottom
+            )
+        }
+
+        fun bindTo(item: Item?) {
+            this.item = item
+            // setting placeholder height to 0 creates a weird jumping bug, investigate
+            itemView.layoutParams.height = item?.height ?: RV_HEIGHT / 10
+        }
+    }
+
+    /**
+     * Checks whether a [TestCoroutineDispatcher] has any pending actions using reflection :)
+     */
+    @OptIn(InternalCoroutinesApi::class)
+    private val TestCoroutineDispatcher.isNotEmpty: Boolean
+        get() {
+            this@isNotEmpty::class.java.getDeclaredField("queue").let {
+                it.isAccessible = true
+                val heap = it.get(this) as ThreadSafeHeap<*>
+                return !heap.isEmpty
+            }
+        }
+
+    /**
+     * Checks whether a [TestCoroutineDispatcher] is paused or not using reflection.
+     */
+    private val TestCoroutineDispatcher.isNotPaused: Boolean
+        get() {
+            this@isNotPaused::class.java.getDeclaredField("dispatchImmediately").let {
+                it.isAccessible = true
+                return it.get(this) as Boolean
+            }
+        }
+
+    data class Item(
+        val id: Int,
+        val height: Int = (RV_HEIGHT / 10) + (1 + (id % 10))
+    ) {
+        companion object {
+            val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Item>() {
+                override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
+                    return oldItem.id == newItem.id
+                }
+
+                override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
+                    return oldItem == newItem
+                }
+            }
+        }
+    }
+
+    inner class TestAdapter : PagingDataAdapter<Item, ItemViewHolder>(
+        diffCallback = Item.DIFF_CALLBACK,
+        mainDispatcher = mainDispatcher,
+        workerDispatcher = backgroundDispatcher
+    ) {
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
+            return ItemViewHolder(parent.context)
+        }
+
+        override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
+            holder.bindTo(getItem(position))
+        }
+
+        fun triggerItemLoad(pos: Int) = super.getItem(pos)
+    }
+
+    class ItemPagingSource(
+        private val context: CoroutineContext,
+        private val items: List<Item>
+    ) : PagingSource<Int, Item>() {
+        override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
+            return withContext(context) {
+                val key = params.key ?: 0
+                val isPrepend = params is LoadParams.Prepend
+                val start = if (isPrepend) key - params.loadSize + 1 else key
+                val end = if (isPrepend) key + 1 else key + params.loadSize
+
+                LoadResult.Page(
+                    data = items.subList(maxOf(0, start), minOf(end, items.size)),
+                    prevKey = if (start > 0) start - 1 else null,
+                    nextKey = if (end < items.size) end else null,
+                    itemsBefore = maxOf(0, start),
+                    itemsAfter = maxOf(0, items.size - end)
+                )
+            }
+        }
+    }
+
+    /**
+     * Snapshot of an item in RecyclerView.
+     */
+    data class PositionSnapshot(
+        val item: Item,
+        val top: Int,
+        val bottom: Int
+    )
+
+    /**
+     * RecyclerView class that allows saving and restoring state.
+     */
+    class TestRecyclerView(context: Context) : RecyclerView(context) {
+        fun restoreState(state: Parcelable?) {
+            super.onRestoreInstanceState(state)
+        }
+
+        fun saveState(): Parcelable? {
+            return super.onSaveInstanceState()
+        }
+    }
+
+    /**
+     * A layout manager that tracks whether state is restored or not so that we can assert on it.
+     */
+    class RestoreAwareLayoutManager(context: Context) : LinearLayoutManager(context) {
+        var restoredState = false
+        override fun onRestoreInstanceState(state: Parcelable?) {
+            super.onRestoreInstanceState(state)
+            restoredState = true
+        }
+    }
+
+    companion object {
+        private const val RV_HEIGHT = 200
+        private const val RV_WIDTH = 100
+    }
+}
\ No newline at end of file
diff --git a/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt b/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
index 2a5de36..8cb757b 100644
--- a/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
+++ b/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
@@ -71,15 +71,18 @@
             previousList: NullPaddedList<T>,
             newList: NullPaddedList<T>,
             newCombinedLoadStates: CombinedLoadStates,
-            lastAccessedIndex: Int
+            lastAccessedIndex: Int,
+            onListPresentable: () -> Unit,
         ) = when {
             // fast path for no items -> some items
             previousList.size == 0 -> {
+                onListPresentable()
                 differCallback.onInserted(0, newList.size)
                 null
             }
             // fast path for some items -> no items
             newList.size == 0 -> {
+                onListPresentable()
                 differCallback.onRemoved(0, previousList.size)
                 null
             }
@@ -87,6 +90,7 @@
                 val diffResult = withContext(workerDispatcher) {
                     previousList.computeDiff(newList, diffCallback)
                 }
+                onListPresentable()
                 previousList.dispatchDiff(updateCallback, newList, diffResult)
                 previousList.transformAnchorIndex(
                     diffResult = diffResult,
diff --git a/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt b/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
index 32bfa57..ab56fae 100644
--- a/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
+++ b/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
@@ -41,10 +41,12 @@
     private val fetchDispatcher: CoroutineDispatcher
 ) : LiveData<PagedList<Value>>(
     InitialPagedList(
-        pagingSourceFactory(),
-        coroutineScope,
-        config,
-        initialKey
+        pagingSource = pagingSourceFactory(),
+        coroutineScope = coroutineScope,
+        notifyDispatcher = notifyDispatcher,
+        backgroundDispatcher = fetchDispatcher,
+        config = config,
+        initialLastKey = initialKey
     )
 ) {
     private var currentData: PagedList<Value>
diff --git a/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt b/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
index 3f19e4b..82892f3 100644
--- a/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
+++ b/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
@@ -46,6 +46,14 @@
  * compute fine grained updates as updated content in the form of new PagingData objects are
  * received.
  *
+ * *State Restoration*: To be able to restore [RecyclerView] state (e.g. scroll position) after a
+ * configuration change / application recreate, [PagingDataAdapter] calls
+ * [RecyclerView.Adapter.setStateRestorationPolicy] with
+ * [RecyclerView.Adapter.StateRestorationPolicy.PREVENT] upon initialization and waits for the
+ * first page to load before allowing state restoration.
+ * Any other call to [RecyclerView.Adapter.setStateRestorationPolicy] by the application will
+ * disable this logic and will rely on the user set value.
+ *
  * @sample androidx.paging.samples.pagingDataAdapterSample
  */
 abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> @JvmOverloads constructor(
@@ -53,6 +61,41 @@
     mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
     workerDispatcher: CoroutineDispatcher = Dispatchers.Default
 ) : RecyclerView.Adapter<VH>() {
+
+    init {
+        super.setStateRestorationPolicy(StateRestorationPolicy.PREVENT)
+        // prevent state restoration and then watch for the first insert event.
+        // differ calls this with the inserted page even when it is empty, which is what we want
+        // here
+        @Suppress("LeakingThis")
+        registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+            override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+                considerAllowingStateRestoration()
+                unregisterAdapterDataObserver(this)
+                super.onItemRangeInserted(positionStart, itemCount)
+            }
+
+            private fun considerAllowingStateRestoration() {
+                if (stateRestorationPolicy == StateRestorationPolicy.PREVENT &&
+                    !userSetRestorationPolicy
+                ) {
+                    this@PagingDataAdapter.setStateRestorationPolicy(StateRestorationPolicy.ALLOW)
+                }
+            }
+        })
+    }
+
+    /**
+     * Track whether developer called [setStateRestorationPolicy] or not to decide whether the
+     * automated state restoration should apply or not.
+     */
+    private var userSetRestorationPolicy = false
+
+    override fun setStateRestorationPolicy(strategy: StateRestorationPolicy) {
+        userSetRestorationPolicy = true
+        super.setStateRestorationPolicy(strategy)
+    }
+
     private val differ = AsyncPagingDataDiffer(
         diffCallback = diffCallback,
         updateCallback = AdapterListUpdateCallback(this),
diff --git a/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt b/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
index 0a75c08..2959747 100644
--- a/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
+++ b/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
@@ -302,7 +302,7 @@
             ?: dataSourceFactory?.asPagingSourceFactory(fetchDispatcher)
 
         check(pagingSourceFactory != null) {
-            "LivePagedList cannot be built without a PagingSourceFactory or DataSource.Factory"
+            "RxPagedList cannot be built without a PagingSourceFactory or DataSource.Factory"
         }
 
         return Observable
@@ -356,10 +356,12 @@
 
         init {
             currentData = InitialPagedList(
-                pagingSourceFactory(),
-                GlobalScope,
-                config,
-                initialLoadKey
+                pagingSource = pagingSourceFactory(),
+                coroutineScope = GlobalScope,
+                notifyDispatcher = notifyDispatcher,
+                backgroundDispatcher = fetchDispatcher,
+                config = config,
+                initialLastKey = initialLoadKey
             )
             currentData.setRetryCallback(refreshRetryCallback)
         }
diff --git a/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt b/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
index 4a1e45b..484e6d3 100644
--- a/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
+++ b/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
@@ -68,8 +68,11 @@
             }
 
             private fun loadInitial(params: LoadParams<Int>): LoadResult<Int, String> {
-                @Suppress("DEPRECATION")
-                assertEquals(2, params.pageSize)
+                if (params is LoadParams.Refresh) {
+                    assertEquals(6, params.loadSize)
+                } else {
+                    assertEquals(2, params.loadSize)
+                }
 
                 throwable?.let { error ->
                     throwable = null
diff --git a/paging/rxjava2/src/test/java/androidx/paging/RxPagingSourceTest.kt b/paging/rxjava2/src/test/java/androidx/paging/RxPagingSourceTest.kt
index 0068cfb..a6c5b4e 100644
--- a/paging/rxjava2/src/test/java/androidx/paging/RxPagingSourceTest.kt
+++ b/paging/rxjava2/src/test/java/androidx/paging/RxPagingSourceTest.kt
@@ -53,14 +53,14 @@
 
     @Test
     fun basic() = runBlocking {
-        val params = PagingSource.LoadParams.Refresh(0, 2, false, 2)
+        val params = PagingSource.LoadParams.Refresh(0, 2, false)
         assertEquals(pagingSource.load(params), rxPagingSource.load(params))
     }
 
     @Test
     fun error() {
         runBlocking {
-            val params = PagingSource.LoadParams.Refresh<Int>(null, 2, false, 2)
+            val params = PagingSource.LoadParams.Refresh<Int>(null, 2, false)
             assertFailsWith<NullPointerException> { pagingSource.load(params) }
             assertFailsWith<NullPointerException> { rxPagingSource.load(params) }
         }
diff --git a/paging/rxjava3/src/test/java/androidx/paging/RxPagingSourceTest.kt b/paging/rxjava3/src/test/java/androidx/paging/RxPagingSourceTest.kt
index 149b4bf..f7881b1 100644
--- a/paging/rxjava3/src/test/java/androidx/paging/RxPagingSourceTest.kt
+++ b/paging/rxjava3/src/test/java/androidx/paging/RxPagingSourceTest.kt
@@ -53,14 +53,14 @@
 
     @Test
     fun basic() = runBlocking {
-        val params = PagingSource.LoadParams.Refresh(0, 2, false, 2)
+        val params = PagingSource.LoadParams.Refresh(0, 2, false)
         assertEquals(pagingSource.load(params), rxPagingSource.load(params))
     }
 
     @Test
     fun error() {
         runBlocking {
-            val params = PagingSource.LoadParams.Refresh<Int>(null, 2, false, 2)
+            val params = PagingSource.LoadParams.Refresh<Int>(null, 2, false)
             assertFailsWith<NullPointerException> { pagingSource.load(params) }
             assertFailsWith<NullPointerException> { rxPagingSource.load(params) }
         }
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 03609a8..0c651335 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -28,7 +28,7 @@
 androidx.enableDocumentation=false
 # Disable coverage
 androidx.coverageEnabled=false
-androidx.playground.snapshotBuildId=6990786
+androidx.playground.snapshotBuildId=7012196
 androidx.playground.metalavaBuildId=6990868
 androidx.playground.dokkaBuildId=6915080
 androidx.studio.type=playground
diff --git a/room/compiler-processing-testing/build.gradle b/room/compiler-processing-testing/build.gradle
index 0edce59..44e7780 100644
--- a/room/compiler-processing-testing/build.gradle
+++ b/room/compiler-processing-testing/build.gradle
@@ -26,12 +26,18 @@
 
 dependencies {
     implementation("androidx.annotation:annotation:1.1.0")
-    implementation(project(":room:room-compiler-processing"))
+    api(project(":room:room-compiler-processing"))
     implementation(KOTLIN_STDLIB)
     implementation(KOTLIN_KSP_API)
-    testImplementation(KOTLIN_KSP)
+    implementation(KOTLIN_KSP)
     implementation(GOOGLE_COMPILE_TESTING)
     implementation(KOTLIN_COMPILE_TESTING_KSP)
+    // specify these because KSP do not specify them and we might get an older version from kotlin
+    // compile testing
+    // https://github.com/google/ksp/issues/187
+    implementation(KOTLIN_COMPILER_EMBEDDABLE)
+    implementation(KOTLIN_COMPILER_DAEMON_EMBEDDABLE)
+    implementation(KOTLIN_ANNOTATION_PROCESSING_EMBEDDABLE)
 }
 
 androidx {
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
index 0ac5928..54fc124 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
@@ -16,21 +16,27 @@
 
 package androidx.room.compiler.processing
 
+import androidx.room.compiler.processing.util.RecordingXMessager
 import androidx.room.compiler.processing.util.XTestInvocation
-import java.lang.AssertionError
 import javax.lang.model.SourceVersion
 
 class SyntheticJavacProcessor(
-    val handler: (XTestInvocation) -> Unit
-) : JavacTestProcessor() {
+    val handler: (XTestInvocation) -> Unit,
+) : JavacTestProcessor(), SyntheticProcessor {
+    override val invocationInstances = mutableListOf<XTestInvocation>()
     private var result: Result<Unit>? = null
+    override val messageWatcher = RecordingXMessager()
 
     override fun doProcess(annotations: Set<XTypeElement>, roundEnv: XRoundEnv): Boolean {
+        val xEnv = XProcessingEnv.create(processingEnv)
+        xEnv.messager.addMessageWatcher(messageWatcher)
         result = kotlin.runCatching {
             handler(
                 XTestInvocation(
-                    processingEnv = XProcessingEnv.create(processingEnv)
-                )
+                    processingEnv = xEnv
+                ).also {
+                    invocationInstances.add(it)
+                }
             )
         }
         return true
@@ -42,7 +48,7 @@
 
     override fun getSupportedAnnotationTypes() = setOf("*")
 
-    fun throwIfFailed() {
+    override fun throwIfFailed() {
         val result = checkNotNull(result) {
             "did not compile"
         }
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt
index 106d6b1..68ec813 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing
 
+import androidx.room.compiler.processing.util.RecordingXMessager
 import androidx.room.compiler.processing.util.XTestInvocation
 import com.google.devtools.ksp.processing.CodeGenerator
 import com.google.devtools.ksp.processing.KSPLogger
@@ -24,11 +25,14 @@
 
 class SyntheticKspProcessor(
     private val handler: (XTestInvocation) -> Unit
-) : SymbolProcessor {
+) : SymbolProcessor, SyntheticProcessor {
+    override val invocationInstances = mutableListOf<XTestInvocation>()
     private var result: Result<Unit>? = null
     private lateinit var options: Map<String, String>
     private lateinit var codeGenerator: CodeGenerator
     private lateinit var logger: KSPLogger
+    override val messageWatcher = RecordingXMessager()
+
     override fun finish() {
     }
 
@@ -44,21 +48,25 @@
     }
 
     override fun process(resolver: Resolver) {
+        val xEnv = XProcessingEnv.create(
+            options,
+            resolver,
+            codeGenerator,
+            logger
+        )
+        xEnv.messager.addMessageWatcher(messageWatcher)
         result = kotlin.runCatching {
             handler(
                 XTestInvocation(
-                    processingEnv = XProcessingEnv.create(
-                        options,
-                        resolver,
-                        codeGenerator,
-                        logger
-                    )
-                )
+                    processingEnv = xEnv
+                ).also {
+                    invocationInstances.add(it)
+                }
             )
         }
     }
 
-    fun throwIfFailed() {
+    override fun throwIfFailed() {
         val result = checkNotNull(result) {
             "did not compile"
         }
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt
new file mode 100644
index 0000000..05cc826
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing
+
+import androidx.room.compiler.processing.util.RecordingXMessager
+import androidx.room.compiler.processing.util.XTestInvocation
+
+/**
+ * Common interface for SyntheticProcessors that we create for testing.
+ */
+internal interface SyntheticProcessor {
+    /**
+     * List of invocations that was sent to the test code.
+     *
+     * The test code can register assertions on the compilation result, which is why we need this
+     * list (to run assertions after compilation).
+     */
+    val invocationInstances: List<XTestInvocation>
+
+    /**
+     * The recorder for messages where we'll grab the diagnostics.
+     */
+    val messageWatcher: RecordingXMessager
+
+    /**
+     * Should throw if processor did throw an exception.
+     * When assertions fail, we don't fail the compilation to keep the stack trace, instead,
+     * dispatch them afterwards.
+     */
+    fun throwIfFailed()
+}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt
new file mode 100644
index 0000000..3dbbff7
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util
+
+import androidx.room.compiler.processing.SyntheticJavacProcessor
+import androidx.room.compiler.processing.SyntheticProcessor
+import androidx.room.compiler.processing.util.runner.CompilationTestRunner
+import com.google.common.truth.Fact.simpleFact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
+import com.google.testing.compile.Compilation
+import com.google.testing.compile.CompileTester
+import com.tschuchort.compiletesting.KotlinCompilation
+import javax.tools.Diagnostic
+
+/**
+ * Holds the information about a test compilation result.
+ */
+abstract class CompilationResult internal constructor(
+    /**
+     * The test infra which run this test
+     */
+    internal val testRunnerName: String,
+    /**
+     * The [SyntheticProcessor] used in this compilation.
+     */
+    internal val processor: SyntheticProcessor,
+    /**
+     * True if compilation result was success.
+     */
+    internal val successfulCompilation: Boolean,
+) {
+    private val diagnostics = processor.messageWatcher.diagnostics()
+
+    fun diagnosticsOfKind(kind: Diagnostic.Kind) = diagnostics[kind].orEmpty()
+
+    override fun toString(): String {
+        return buildString {
+            appendLine("CompilationResult (with $testRunnerName)")
+            Diagnostic.Kind.values().forEach { kind ->
+                val messages = diagnosticsOfKind(kind)
+                appendLine("${kind.name}: ${messages.size}")
+                messages.forEach {
+                    appendLine(it)
+                }
+                appendLine()
+            }
+        }
+    }
+}
+
+/**
+ * Truth subject that can run assertions on the [CompilationResult].
+ * see: [XTestInvocation.assertCompilationResult]
+ */
+class CompilationResultSubject(
+    failureMetadata: FailureMetadata,
+    val compilationResult: CompilationResult,
+) : Subject<CompilationResultSubject, CompilationResult>(
+    failureMetadata, compilationResult
+) {
+    /**
+     * set to true if any assertion on the subject requires it to fail (e.g. looking for errors)
+     */
+    internal var shouldSucceed: Boolean = true
+
+    /**
+     * Asserts that compilation did fail. This covers the cases where the processor won't print
+     * any diagnostics but compilation will still fail (e.g. bad generated code).
+     *
+     * @see hasError
+     */
+    fun compilationDidFail() = chain {
+        shouldSucceed = false
+    }
+
+    /**
+     * Asserts that compilation has a warning with the given text.
+     *
+     * @see hasError
+     */
+    fun hasWarning(expected: String) = chain {
+        hasDiagnosticWithMessage(
+            kind = Diagnostic.Kind.WARNING,
+            expected = expected
+        ) {
+            "expected warning: $expected"
+        }
+    }
+
+    /**
+     * Asserts that compilation has an error with the given text.
+     *
+     * @see hasWarning
+     */
+    fun hasError(expected: String) = chain {
+        shouldSucceed = false
+        hasDiagnosticWithMessage(
+            kind = Diagnostic.Kind.ERROR,
+            expected = expected
+        ) {
+            "expected error: $expected"
+        }
+    }
+
+    /**
+     * Asserts that compilation has at least one diagnostics message with kind error.
+     *
+     * @see compilationDidFail
+     * @see hasWarning
+     */
+    fun hasError() = chain {
+        shouldSucceed = false
+        if (actual().diagnosticsOfKind(Diagnostic.Kind.ERROR).isEmpty()) {
+            failWithActual(
+                simpleFact("expected at least one failure message")
+            )
+        }
+    }
+
+    /**
+     * Called after handler is invoked to check its compilation failure assertion against the
+     * compilation result.
+     */
+    internal fun assertCompilationResult() {
+        if (compilationResult.successfulCompilation != shouldSucceed) {
+            failWithActual(
+                simpleFact(
+                    "expected compilation result to be: $shouldSucceed but was " +
+                        "${compilationResult.successfulCompilation}"
+                )
+            )
+        }
+    }
+
+    private fun hasDiagnosticWithMessage(
+        kind: Diagnostic.Kind,
+        expected: String,
+        buildErrorMessage: () -> String
+    ) {
+        val diagnostics = compilationResult.diagnosticsOfKind(kind)
+        if (diagnostics.any { it.msg == expected }) {
+            return
+        }
+        failWithActual(simpleFact(buildErrorMessage()))
+    }
+
+    private fun chain(
+        block: () -> Unit
+    ): CompileTester.ChainingClause<CompilationResultSubject> {
+        block()
+        return CompileTester.ChainingClause<CompilationResultSubject> {
+            this
+        }
+    }
+
+    companion object {
+        private val FACTORY =
+            Factory<CompilationResultSubject, CompilationResult> { metadata, actual ->
+                CompilationResultSubject(metadata, actual)
+            }
+
+        fun assertThat(
+            compilationResult: CompilationResult
+        ): CompilationResultSubject {
+            return Truth.assertAbout(FACTORY).that(
+                compilationResult
+            )
+        }
+    }
+}
+
+internal class JavaCompileTestingCompilationResult(
+    testRunner: CompilationTestRunner,
+    @Suppress("unused")
+    private val delegate: Compilation,
+    processor: SyntheticJavacProcessor
+) : CompilationResult(
+    testRunnerName = testRunner.name,
+    processor = processor,
+    successfulCompilation = delegate.status() == Compilation.Status.SUCCESS
+)
+
+internal class KotlinCompileTestingCompilationResult(
+    testRunner: CompilationTestRunner,
+    @Suppress("unused")
+    private val delegate: KotlinCompilation.Result,
+    processor: SyntheticProcessor,
+    successfulCompilation: Boolean
+) : CompilationResult(
+    testRunnerName = testRunner.name,
+    processor = processor,
+    successfulCompilation = successfulCompilation
+)
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
similarity index 73%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
index f9cb2fe..34ef8e3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
@@ -14,7 +14,14 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+package androidx.room.compiler.processing.util
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.room.compiler.processing.XElement
+
+/**
+ * Holder for diagnostics messages
+ */
+data class DiagnosticMessage(
+    val msg: String,
+    val element: XElement?
+)
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
index 076b531..bbf2888 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
@@ -16,275 +16,147 @@
 
 package androidx.room.compiler.processing.util
 
-import androidx.room.compiler.processing.SyntheticJavacProcessor
-import androidx.room.compiler.processing.SyntheticKspProcessor
-import com.google.common.truth.Truth
+import androidx.room.compiler.processing.util.runner.CompilationTestRunner
+import androidx.room.compiler.processing.util.runner.JavacCompilationTestRunner
+import androidx.room.compiler.processing.util.runner.KaptCompilationTestRunner
+import androidx.room.compiler.processing.util.runner.KspCompilationTestRunner
+import androidx.room.compiler.processing.util.runner.TestCompilationParameters
 import com.google.common.truth.Truth.assertThat
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.google.common.truth.Truth.assertWithMessage
 import com.tschuchort.compiletesting.KotlinCompilation
-import com.tschuchort.compiletesting.SourceFile
-import com.tschuchort.compiletesting.kspSourcesDir
-import com.tschuchort.compiletesting.symbolProcessors
 import java.io.File
 
-// TODO get rid of these once kotlin compile testing supports two step compilation for KSP.
-//  https://github.com/tschuchortdev/kotlin-compile-testing/issues/72
-private val KotlinCompilation.kspJavaSourceDir: File
-    get() = kspSourcesDir.resolve("java")
+private fun runTests(
+    params: TestCompilationParameters,
+    vararg runners: CompilationTestRunner
+) {
+    val runCount = runners.count { runner ->
+        if (runner.canRun(params)) {
+            val compilationResult = runner.compile(params)
+            val subject = CompilationResultSubject.assertThat(compilationResult)
+            // if any assertion failed, throw first those.
+            compilationResult.processor.throwIfFailed()
 
-private val KotlinCompilation.kspKotlinSourceDir: File
-    get() = kspSourcesDir.resolve("kotlin")
+            compilationResult.processor.invocationInstances.forEach {
+                it.runPostCompilationChecks(subject)
+            }
+            assertWithMessage(
+                "compilation should've run the processor callback at least once"
+            ).that(
+                compilationResult.processor.invocationInstances
+            ).isNotEmpty()
 
-private fun compileSources(
-    sources: List<Source>,
-    classpath: List<File>,
+            subject.assertCompilationResult()
+            true
+        } else {
+            false
+        }
+    }
+    // make sure some tests did run
+    assertThat(runCount).isGreaterThan(0)
+}
+
+fun runProcessorTestWithoutKsp(
+    sources: List<Source> = emptyList(),
+    classpath: List<File> = emptyList(),
     handler: (XTestInvocation) -> Unit
-): Pair<SyntheticJavacProcessor, CompileTester> {
-    val syntheticJavacProcessor = SyntheticJavacProcessor(handler)
-    return syntheticJavacProcessor to Truth.assertAbout(
-        JavaSourcesSubjectFactory.javaSources()
-    ).that(
-        sources.map {
-            it.toJFO()
-        }
-    ).apply {
-        if (classpath.isNotEmpty()) {
-            withClasspath(classpath)
-        }
-    }.processedWith(
-        syntheticJavacProcessor
+) {
+    runTests(
+        params = TestCompilationParameters(
+            sources = sources,
+            classpath = classpath,
+            handler = handler
+        ),
+        JavacCompilationTestRunner,
+        KaptCompilationTestRunner
     )
 }
 
-private fun compileWithKapt(
-    sources: List<Source>,
-    classpath: List<File>,
-    handler: (XTestInvocation) -> Unit
-): Pair<SyntheticJavacProcessor, KotlinCompilation> {
-    val syntheticJavacProcessor = SyntheticJavacProcessor(handler)
-    val compilation = KotlinCompilation()
-    sources.forEach {
-        compilation.workingDir.resolve("sources")
-            .resolve(it.relativePath())
-            .parentFile
-            .mkdirs()
-    }
-    compilation.sources = sources.map {
-        it.toKotlinSourceFile()
-    }
-    compilation.annotationProcessors = listOf(syntheticJavacProcessor)
-    compilation.inheritClassPath = true
-    compilation.verbose = false
-    compilation.classpaths += classpath
-
-    return syntheticJavacProcessor to compilation
-}
-
-private fun compileWithKsp(
-    sources: List<Source>,
-    classpath: List<File>,
-    handler: (XTestInvocation) -> Unit
-): Pair<SyntheticKspProcessor, KotlinCompilation.Result> {
-    @Suppress("NAME_SHADOWING")
-    val sources = if (sources.none { it is Source.KotlinSource }) {
-        // looks like this requires a kotlin source file
-        // see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/57
-        sources + Source.kotlin("placeholder.kt", "")
-    } else {
-        sources
-    }
-    val syntheticKspProcessor = SyntheticKspProcessor(handler)
-    fun prepareCompilation(): KotlinCompilation {
-        val compilation = KotlinCompilation()
-        sources.forEach {
-            compilation.workingDir.resolve("sources")
-                .resolve(it.relativePath())
-                .parentFile
-                .mkdirs()
-        }
-        compilation.sources = sources.map {
-            it.toKotlinSourceFile()
-        }
-        compilation.jvmDefault = "enable"
-        compilation.jvmTarget = "1.8"
-        compilation.inheritClassPath = true
-        compilation.verbose = false
-        compilation.classpaths += classpath
-        return compilation
-    }
-
-    val kspCompilation = prepareCompilation()
-    kspCompilation.symbolProcessors = listOf(syntheticKspProcessor)
-    kspCompilation.compile()
-    // ignore KSP result for now because KSP stops compilation, which might create false negatives
-    // when java code accesses kotlin code.
-    // TODO:  fix once https://github.com/tschuchortdev/kotlin-compile-testing/issues/72 is fixed
-
-    // after ksp, compile without ksp with KSP's output as input
-    val finalCompilation = prepareCompilation()
-    // build source files from generated code
-    finalCompilation.sources += kspCompilation.kspJavaSourceDir.collectSourceFiles() +
-        kspCompilation.kspKotlinSourceDir.collectSourceFiles()
-    return syntheticKspProcessor to finalCompilation.compile()
-}
-
-private fun File.collectSourceFiles(): List<SourceFile> {
-    return walkTopDown().filter {
-        it.isFile
-    }.map { file ->
-        SourceFile.fromPath(file)
-    }.toList()
-}
-
+/**
+ * Runs the compilation test with all 3 backends (javac, kapt, ksp) if possible (e.g. javac
+ * cannot test kotlin sources).
+ *
+ * The [handler] will be invoked for each compilation hence it should be repeatable.
+ *
+ * To assert on the compilation results, [handler] can call
+ * [XTestInvocation.assertCompilationResult] where it will receive a subject for post compilation
+ * assertions.
+ *
+ * By default, the compilation is expected to succeed. If it should fail, there must be an
+ * assertion on [XTestInvocation.assertCompilationResult] which expects a failure (e.g. checking
+ * errors).
+ */
 fun runProcessorTest(
     sources: List<Source> = emptyList(),
     classpath: List<File> = emptyList(),
     handler: (XTestInvocation) -> Unit
 ) {
-    @Suppress("NAME_SHADOWING")
-    val sources = if (sources.isEmpty()) {
-        // synthesize a source to trigger compilation
-        listOf(
-            Source.java(
-                "foo.bar.SyntheticSource",
-                """
-            package foo.bar;
-            public class SyntheticSource {}
-                """.trimIndent()
-            )
-        )
-    } else {
-        sources
-    }
-    // we can compile w/ javac only if all code is in java
-    if (sources.canCompileWithJava()) {
-        runJavaProcessorTest(
+    runTests(
+        params = TestCompilationParameters(
             sources = sources,
             classpath = classpath,
-            handler = handler,
-            succeed = true
-        )
-    }
-    runKaptTest(
-        sources = sources,
-        classpath = classpath,
-        handler = handler,
-        succeed = true
+            handler = handler
+        ),
+        JavacCompilationTestRunner,
+        KaptCompilationTestRunner,
+        KspCompilationTestRunner
     )
 }
 
 /**
- * This method is oddly named instead of being an overload on runProcessorTest to easily track
- * which tests started to support KSP.
+ * Runs the test only with javac compilation backend.
  *
- * Eventually, it will be merged with runProcessorTest when all tests pass with KSP.
+ * @see runProcessorTest
  */
-fun runProcessorTestIncludingKsp(
-    sources: List<Source> = emptyList(),
-    classpath: List<File> = emptyList(),
-    handler: (XTestInvocation) -> Unit
-) {
-    runProcessorTest(
-        sources = sources,
-        classpath = classpath,
-        handler = handler
-    )
-    runKspTest(
-        sources = sources,
-        classpath = classpath,
-        succeed = true,
-        handler = handler
-    )
-}
-
-fun runProcessorTestForFailedCompilation(
-    sources: List<Source>,
-    classpath: List<File> = emptyList(),
-    handler: (XTestInvocation) -> Unit
-) {
-    if (sources.canCompileWithJava()) {
-        // run with java processor
-        runJavaProcessorTest(
-            sources = sources,
-            classpath = classpath,
-            handler = handler,
-            succeed = false
-        )
-    }
-    // now run with kapt
-    runKaptTest(
-        sources = sources,
-        classpath = classpath,
-        handler = handler,
-        succeed = false
-    )
-}
-
-fun runProcessorTestForFailedCompilationIncludingKsp(
-    sources: List<Source>,
-    classpath: List<File>,
-    handler: (XTestInvocation) -> Unit
-) {
-    runProcessorTestForFailedCompilation(
-        sources = sources,
-        classpath = classpath,
-        handler = handler
-    )
-    // now run with ksp
-    runKspTest(
-        sources = sources,
-        classpath = classpath,
-        handler = handler,
-        succeed = false
-    )
-}
-
 fun runJavaProcessorTest(
     sources: List<Source>,
     classpath: List<File>,
-    succeed: Boolean,
     handler: (XTestInvocation) -> Unit
 ) {
-    val (syntheticJavacProcessor, compileTester) = compileSources(sources, classpath, handler)
-    if (succeed) {
-        compileTester.compilesWithoutError()
-    } else {
-        compileTester.failsToCompile()
-    }
-    syntheticJavacProcessor.throwIfFailed()
+    runTests(
+        params = TestCompilationParameters(
+            sources = sources,
+            classpath = classpath,
+            handler = handler
+        ),
+        JavacCompilationTestRunner
+    )
 }
 
+/**
+ * Runs the test only with kapt compilation backend
+ */
 fun runKaptTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
-    succeed: Boolean = true,
     handler: (XTestInvocation) -> Unit
 ) {
-    // now run with kapt
-    val (kaptProcessor, kotlinCompilation) = compileWithKapt(sources, classpath, handler)
-    val compilationResult = kotlinCompilation.compile()
-    if (succeed) {
-        assertThat(compilationResult.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
-    } else {
-        assertThat(compilationResult.exitCode).isNotEqualTo(KotlinCompilation.ExitCode.OK)
-    }
-    kaptProcessor.throwIfFailed()
+    runTests(
+        params = TestCompilationParameters(
+            sources = sources,
+            classpath = classpath,
+            handler = handler
+        ),
+        KaptCompilationTestRunner
+    )
 }
 
+/**
+ * Runs the test only with ksp compilation backend
+ */
 fun runKspTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
-    succeed: Boolean = true,
     handler: (XTestInvocation) -> Unit
 ) {
-    val (kspProcessor, compilationResult) = compileWithKsp(sources, classpath, handler)
-    if (succeed) {
-        assertThat(compilationResult.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
-    } else {
-        assertThat(compilationResult.exitCode).isNotEqualTo(KotlinCompilation.ExitCode.OK)
-    }
-    kspProcessor.throwIfFailed()
+    runTests(
+        params = TestCompilationParameters(
+            sources = sources,
+            classpath = classpath,
+            handler = handler
+        ),
+        KspCompilationTestRunner
+    )
 }
 
 /**
@@ -314,5 +186,3 @@
     }
     return compilation.classesDir
 }
-
-private fun List<Source>.canCompileWithJava() = all { it is Source.JavaSource }
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt
new file mode 100644
index 0000000..c2aa5dc
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util
+
+import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XMessager
+import javax.tools.Diagnostic
+
+/**
+ * An XMessager implementation that holds onto dispatched diagnostics.
+ */
+class RecordingXMessager : XMessager() {
+    private val diagnostics = mutableMapOf<Diagnostic.Kind, MutableList<DiagnosticMessage>>()
+
+    fun diagnostics(): Map<Diagnostic.Kind, List<DiagnosticMessage>> = diagnostics
+
+    override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+        diagnostics.getOrPut(
+            kind
+        ) {
+            mutableListOf()
+        }.add(
+            DiagnosticMessage(
+                msg = msg,
+                element = element
+            )
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
index 267c140..d7d0e75 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.util
 
 import androidx.room.compiler.processing.XProcessingEnv
+import kotlin.reflect.KClass
 
 /**
  * Data holder for XProcessing tests to access the processing environment.
@@ -24,6 +25,48 @@
 class XTestInvocation(
     val processingEnv: XProcessingEnv,
 ) {
+    /**
+     * Extension mechanism to allow putting objects into invocation that can be retrieved later.
+     */
+    private val userData = mutableMapOf<KClass<*>, Any>()
+
+    private val postCompilationAssertions = mutableListOf<CompilationResultSubject.() -> Unit>()
     val isKsp: Boolean
         get() = processingEnv.backend == XProcessingEnv.Backend.KSP
+
+    /**
+     * Registers a block that will be called with a [CompilationResultSubject] when compilation
+     * finishes.
+     *
+     * Note that it is not safe to access the environment in this block.
+     */
+    fun assertCompilationResult(block: CompilationResultSubject.() -> Unit) {
+        postCompilationAssertions.add(block)
+    }
+
+    internal fun runPostCompilationChecks(
+        compilationResultSubject: CompilationResultSubject
+    ) {
+        postCompilationAssertions.forEach {
+            it(compilationResultSubject)
+        }
+    }
+
+    fun <T : Any> getUserData(key: KClass<T>): T? {
+        @Suppress("UNCHECKED_CAST")
+        return userData[key] as T?
+    }
+
+    fun <T : Any> putUserData(key: KClass<T>, value: T) {
+        userData[key] = value
+    }
+
+    fun <T : Any> getOrPutUserData(key: KClass<T>, create: () -> T): T {
+        getUserData(key)?.let {
+            return it
+        }
+        return create().also {
+            putUserData(key, it)
+        }
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
new file mode 100644
index 0000000..257e58a
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util.runner
+
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import java.io.File
+
+/**
+ * Common interface for compilation tests
+ */
+internal interface CompilationTestRunner {
+    // user visible name that we can print in assertions
+    val name: String
+
+    fun canRun(params: TestCompilationParameters): Boolean
+
+    fun compile(params: TestCompilationParameters): CompilationResult
+}
+
+internal data class TestCompilationParameters(
+    val sources: List<Source> = emptyList(),
+    val classpath: List<File> = emptyList(),
+    val handler: (XTestInvocation) -> Unit
+)
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
new file mode 100644
index 0000000..5fdba61
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util.runner
+
+import androidx.room.compiler.processing.SyntheticJavacProcessor
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.JavaCompileTestingCompilationResult
+import androidx.room.compiler.processing.util.Source
+import com.google.testing.compile.Compiler
+
+internal object JavacCompilationTestRunner : CompilationTestRunner {
+
+    override val name: String = "javac"
+
+    override fun canRun(params: TestCompilationParameters): Boolean {
+        return params.sources.all { it is Source.JavaSource }
+    }
+
+    override fun compile(params: TestCompilationParameters): CompilationResult {
+        val syntheticJavacProcessor = SyntheticJavacProcessor(params.handler)
+        val sources = if (params.sources.isEmpty()) {
+            // synthesize a source to trigger compilation
+            listOf(
+                Source.java(
+                    qName = "foo.bar.SyntheticSource",
+                    code = """
+                    package foo.bar;
+                    public class SyntheticSource {}
+                    """.trimIndent()
+                )
+            )
+        } else {
+            params.sources
+        }
+        val compiler = Compiler
+            .javac()
+            .withProcessors(syntheticJavacProcessor)
+            .withOptions("-Xlint")
+            .let {
+                if (params.classpath.isNotEmpty()) {
+                    it.withClasspath(params.classpath)
+                } else {
+                    it
+                }
+            }
+        val javaFileObjects = sources.map {
+            it.toJFO()
+        }
+        val compilation = compiler.compile(javaFileObjects)
+        return JavaCompileTestingCompilationResult(
+            testRunner = this,
+            delegate = compilation,
+            processor = syntheticJavacProcessor
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
new file mode 100644
index 0000000..e6cb5ee
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util.runner
+
+import androidx.room.compiler.processing.SyntheticJavacProcessor
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.KotlinCompileTestingCompilationResult
+import com.tschuchort.compiletesting.KotlinCompilation
+
+internal object KaptCompilationTestRunner : CompilationTestRunner {
+
+    override val name: String = "kapt"
+
+    override fun canRun(params: TestCompilationParameters): Boolean {
+        return true
+    }
+
+    override fun compile(params: TestCompilationParameters): CompilationResult {
+        val syntheticJavacProcessor = SyntheticJavacProcessor(params.handler)
+        val compilation = KotlinCompilation()
+        params.sources.forEach {
+            compilation.workingDir.resolve("sources")
+                .resolve(it.relativePath())
+                .parentFile
+                .mkdirs()
+        }
+        compilation.sources = params.sources.map {
+            it.toKotlinSourceFile()
+        }
+        compilation.annotationProcessors = listOf(syntheticJavacProcessor)
+        compilation.inheritClassPath = true
+        compilation.verbose = false
+        compilation.classpaths += params.classpath
+
+        val result = compilation.compile()
+        return KotlinCompileTestingCompilationResult(
+            testRunner = this,
+            delegate = result,
+            processor = syntheticJavacProcessor,
+            successfulCompilation = result.exitCode == KotlinCompilation.ExitCode.OK
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
new file mode 100644
index 0000000..8ac5ed3
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util.runner
+
+import androidx.room.compiler.processing.SyntheticKspProcessor
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.KotlinCompileTestingCompilationResult
+import androidx.room.compiler.processing.util.Source
+import com.tschuchort.compiletesting.KotlinCompilation
+import com.tschuchort.compiletesting.SourceFile
+import com.tschuchort.compiletesting.kspSourcesDir
+import com.tschuchort.compiletesting.symbolProcessors
+import java.io.File
+import javax.tools.Diagnostic
+
+internal object KspCompilationTestRunner : CompilationTestRunner {
+
+    override val name: String = "ksp"
+
+    override fun canRun(params: TestCompilationParameters): Boolean {
+        return true
+    }
+
+    override fun compile(params: TestCompilationParameters): CompilationResult {
+        @Suppress("NAME_SHADOWING")
+        val sources = if (params.sources.none { it is Source.KotlinSource }) {
+            // looks like this requires a kotlin source file
+            // see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/57
+            params.sources + Source.kotlin("placeholder.kt", "")
+        } else {
+            params.sources
+        }
+        val syntheticKspProcessor = SyntheticKspProcessor(params.handler)
+        fun prepareCompilation(): KotlinCompilation {
+            val compilation = KotlinCompilation()
+            sources.forEach {
+                compilation.workingDir.resolve("sources")
+                    .resolve(it.relativePath())
+                    .parentFile
+                    .mkdirs()
+            }
+            compilation.sources = sources.map {
+                it.toKotlinSourceFile()
+            }
+            compilation.jvmDefault = "enable"
+            compilation.jvmTarget = "1.8"
+            compilation.inheritClassPath = true
+            compilation.verbose = false
+            compilation.classpaths += params.classpath
+            return compilation
+        }
+
+        val kspCompilation = prepareCompilation()
+        kspCompilation.symbolProcessors = listOf(syntheticKspProcessor)
+        kspCompilation.compile()
+        // ignore KSP result for now because KSP stops compilation, which might create false
+        // negatives when java code accesses kotlin code.
+        // TODO:  fix once https://github.com/tschuchortdev/kotlin-compile-testing/issues/72 is
+        //  fixed
+
+        // after ksp, compile without ksp with KSP's output as input
+        val finalCompilation = prepareCompilation()
+        // build source files from generated code
+        finalCompilation.sources += kspCompilation.kspJavaSourceDir.collectSourceFiles() +
+            kspCompilation.kspKotlinSourceDir.collectSourceFiles()
+        val result = finalCompilation.compile()
+        // workaround for: https://github.com/google/ksp/issues/122
+        // KSP does not fail compilation for error diagnostics hence we do it here.
+        val hasErrorDiagnostics = syntheticKspProcessor.messageWatcher
+            .diagnostics()[Diagnostic.Kind.ERROR].orEmpty().isNotEmpty()
+        return KotlinCompileTestingCompilationResult(
+            testRunner = this,
+            delegate = result,
+            processor = syntheticKspProcessor,
+            successfulCompilation = result.exitCode == KotlinCompilation.ExitCode.OK &&
+                !hasErrorDiagnostics
+
+        )
+    }
+
+    // TODO get rid of these once kotlin compile testing supports two step compilation for KSP.
+    //  https://github.com/tschuchortdev/kotlin-compile-testing/issues/72
+    private val KotlinCompilation.kspJavaSourceDir: File
+        get() = kspSourcesDir.resolve("java")
+
+    private val KotlinCompilation.kspKotlinSourceDir: File
+        get() = kspSourcesDir.resolve("kotlin")
+
+    private fun File.collectSourceFiles(): List<SourceFile> {
+        return walkTopDown().filter {
+            it.isFile
+        }.map { file ->
+            SourceFile.fromPath(file)
+        }.toList()
+    }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
new file mode 100644
index 0000000..59dd47f7
--- /dev/null
+++ b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util
+
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.TypeSpec
+import org.junit.Test
+import javax.tools.Diagnostic
+
+class TestRunnerTest {
+    @Test
+    fun generatedBadCode_expected() = generatedBadCode(assertFailure = true)
+
+    @Test(expected = AssertionError::class)
+    fun generatedBadCode_unexpected() = generatedBadCode(assertFailure = false)
+
+    private fun generatedBadCode(assertFailure: Boolean) {
+        runProcessorTest {
+            if (it.processingEnv.findTypeElement("foo.Foo") == null) {
+                val badCode = TypeSpec.classBuilder("Foo").apply {
+                    addStaticBlock(
+                        CodeBlock.of("bad code")
+                    )
+                }.build()
+                val badGeneratedFile = JavaFile.builder("foo", badCode).build()
+                it.processingEnv.filer.write(
+                    badGeneratedFile
+                )
+            }
+            if (assertFailure) {
+                it.assertCompilationResult {
+                    compilationDidFail()
+                }
+            }
+        }
+    }
+
+    @Test
+    fun reportedError_expected() = reportedError(assertFailure = true)
+
+    @Test(expected = AssertionError::class)
+    fun reportedError_unexpected() = reportedError(assertFailure = false)
+
+    fun reportedError(assertFailure: Boolean) {
+        runProcessorTest {
+            it.processingEnv.messager.printMessage(
+                kind = Diagnostic.Kind.ERROR,
+                msg = "reported error"
+            )
+            if (assertFailure) {
+                it.assertCompilationResult {
+                    hasError("reported error")
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/compiler-processing/build.gradle b/room/compiler-processing/build.gradle
index 795a691..0562d8b 100644
--- a/room/compiler-processing/build.gradle
+++ b/room/compiler-processing/build.gradle
@@ -26,34 +26,22 @@
 }
 
 dependencies {
+    api(KOTLIN_STDLIB)
+    api(JAVAPOET)
     implementation("androidx.annotation:annotation:1.1.0")
     implementation(GUAVA)
-    implementation(KOTLIN_STDLIB)
     implementation(AUTO_COMMON)
     implementation(AUTO_VALUE_ANNOTATIONS)
-    implementation(JAVAPOET)
+
     implementation(KOTLIN_METADATA_JVM)
     implementation(INTELLIJ_ANNOTATIONS)
-    implementation(KOTLIN_KSP_API) {
-        version {
-            // TODO remove after KSP versions are fixed
-            //  KSP 1.4 versions are not properly ordered due to rc vs dev versions (dev is latest
-            //  but gradle thinks rc is latest).
-            //  We have to enforce it to ensure the correct version is used.
-            strictly KSP_VERSION
-        }
-    }
+    implementation(KOTLIN_KSP_API)
 
     testImplementation(GOOGLE_COMPILE_TESTING)
     testImplementation(JUNIT)
     testImplementation(JSR250)
     testImplementation(KOTLIN_COMPILE_TESTING_KSP)
-    testImplementation(KOTLIN_KSP) {
-        version {
-            // TODO remove after KSP versions are fixed
-            strictly KSP_VERSION
-        }
-    }
+    testImplementation(KOTLIN_KSP)
     testImplementation(project(":room:room-compiler-processing-testing"))
 }
 
diff --git a/room/compiler-processing/lint-baseline.xml b/room/compiler-processing/lint-baseline.xml
deleted file mode 100644
index 7a7fa1e..0000000
--- a/room/compiler-processing/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" version="4.2.0-alpha15">
-
-    <issue
-        id="BanUncheckedReflection"
-        message="Calling Method.invoke without an SDK check"
-        errorLine1="            return enumClass.getDeclaredMethod(&quot;valueOf&quot;, String::class.java)"
-        errorLine2="                   ^">
-        <location
-            file="src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt"
-            line="260"
-            column="20"/>
-    </issue>
-
-</issues>
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
index 268a5f7..41f6df0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
@@ -17,6 +17,8 @@
 
 import androidx.room.compiler.processing.javac.JavacElement
 import androidx.room.compiler.processing.javac.JavacExecutableElement
+import androidx.room.compiler.processing.ksp.KspMethodElement
+import androidx.room.compiler.processing.ksp.KspMethodType
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
@@ -100,10 +102,18 @@
         elm: XMethodElement,
         owner: XDeclaredType
     ): MethodSpec.Builder {
-        return overridingWithFinalParams(
-            elm,
-            elm.asMemberOf(owner)
-        )
+        val asMember = elm.asMemberOf(owner)
+        return if (elm is KspMethodElement && asMember is KspMethodType) {
+            overridingWithFinalParams(
+                executableElement = elm,
+                resolvedType = asMember.inheritVarianceForOverride()
+            )
+        } else {
+            overridingWithFinalParams(
+                executableElement = elm,
+                resolvedType = asMember
+            )
+        }
     }
 
     private fun overridingWithFinalParams(
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
index 0701c52..29ff818 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
@@ -21,7 +21,8 @@
 /**
  * Logging interface for the processor
  */
-interface XMessager {
+abstract class XMessager {
+    private val watchers = mutableListOf<XMessager>()
     /**
      * Prints the given [msg] to the logs while also associating it with the given [element].
      *
@@ -29,5 +30,20 @@
      * @param msg The actual message to report to the compiler
      * @param element The element with whom the message should be associated with
      */
-    fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement? = null)
+    final fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement? = null) {
+        watchers.forEach {
+            it.printMessage(kind, msg, element)
+        }
+        onPrintMessage(kind, msg, element)
+    }
+
+    abstract fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement? = null)
+
+    fun addMessageWatcher(watcher: XMessager) {
+        watchers.add(watcher)
+    }
+
+    fun removeMessageWatcher(watcher: XMessager) {
+        watchers.remove(watcher)
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
index db41d40..03fc077 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
@@ -166,6 +166,18 @@
      * If this is a wildcard with an extends bound, returns that bounded typed.
      */
     fun extendsBound(): XType?
+
+    /**
+     * Creates a type with nullability [XNullability.NULLABLE] or returns this if the nullability is
+     * already [XNullability.NULLABLE].
+     */
+    fun makeNullable(): XType
+
+    /**
+     * Creates a type with nullability [XNullability.NONNULL] or returns this if the nullability is
+     * already [XNullability.NONNULL].
+     */
+    fun makeNonNullable(): XType
 }
 
 /**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
index 3a79c3a..7e4e1f0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
@@ -56,4 +56,13 @@
     override val equalityItems by lazy {
         arrayOf(typeMirror)
     }
+
+    override fun copyWithNullability(nullability: XNullability): JavacType {
+        return DefaultJavacType(
+            env = env,
+            typeMirror = typeMirror,
+            kotlinType = kotlinType,
+            nullability = nullability
+        )
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt
index bff4938..227d6f6 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt
@@ -91,13 +91,20 @@
             }
             returnType.isArray && returnType.componentType.isAnnotation -> {
                 @Suppress("UNCHECKED_CAST")
-                ListVisitor(env, returnType.componentType as Class<out Annotation>).visit(value)
+                AnnotationListVisitor(env, returnType.componentType as Class<out Annotation>)
+                    .visit(value)
+            }
+            returnType.isArray && returnType.componentType.isEnum -> {
+                @Suppress("UNCHECKED_CAST")
+                EnumListVisitor(returnType.componentType as Class<out Enum<*>>).visit(value)
             }
             returnType.isEnum -> {
                 @Suppress("UNCHECKED_CAST")
                 value.getAsEnum(returnType as Class<out Enum<*>>)
             }
-            else -> throw UnsupportedOperationException("$returnType isn't supported")
+            else -> {
+                throw UnsupportedOperationException("$returnType isn't supported")
+            }
         }
         method.name to result
     }
@@ -230,7 +237,7 @@
 }
 
 @Suppress("DEPRECATION")
-private class ListVisitor<T : Annotation>(
+private class AnnotationListVisitor<T : Annotation>(
     private val env: JavacProcessingEnv,
     private val annotationClass: Class<T>
 ) :
@@ -245,6 +252,24 @@
 }
 
 @Suppress("DEPRECATION")
+private class EnumListVisitor<T : Enum<T>>(private val enumClass: Class<T>) :
+    SimpleAnnotationValueVisitor6<Array<T>, Void?>() {
+    override fun visitArray(
+        values: MutableList<out AnnotationValue>?,
+        void: Void?
+    ): Array<T> {
+        val result = values?.map { it.getAsEnum(enumClass) }
+        @Suppress("UNCHECKED_CAST")
+        val resultArray = java.lang.reflect.Array
+            .newInstance(enumClass, result?.size ?: 0) as Array<T>
+        result?.forEachIndexed { index, value ->
+            resultArray[index] = value
+        }
+        return resultArray
+    }
+}
+
+@Suppress("DEPRECATION")
 private class AnnotationClassVisitor<T : Annotation>(
     private val env: JavacProcessingEnv,
     private val annotationClass: Class<T>
@@ -253,7 +278,7 @@
     override fun visitAnnotation(a: AnnotationMirror?, v: Void?) = a?.box(env, annotationClass)
 }
 
-@Suppress("UNCHECKED_CAST", "DEPRECATION")
+@Suppress("UNCHECKED_CAST", "DEPRECATION", "BanUncheckedReflection")
 private fun <T : Enum<*>> AnnotationValue.getAsEnum(enumClass: Class<T>): T {
     return object : SimpleAnnotationValueVisitor6<T, Void>() {
         override fun visitEnumConstant(value: VariableElement?, p: Void?): T {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
index 87489e2..189f9dc 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
@@ -75,4 +75,14 @@
             elementNullability = componentTypeNullability
         )
     }
+
+    override fun copyWithNullability(nullability: XNullability): JavacType {
+        return JavacArrayType(
+            env = env,
+            typeMirror = typeMirror,
+            nullability = nullability,
+            knownComponentNullability = knownComponentNullability,
+            kotlinType = kotlinType
+        )
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
index bed2e88..348ca07 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
@@ -65,4 +65,13 @@
             )
         }
     }
+
+    override fun copyWithNullability(nullability: XNullability): JavacDeclaredType {
+        return JavacDeclaredType(
+            env = env,
+            typeMirror = typeMirror,
+            kotlinType = kotlinType,
+            nullability = nullability
+        )
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt
index f5120fa..e49a5f7 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt
@@ -27,8 +27,8 @@
 
 internal class JavacProcessingEnvMessager(
     private val processingEnv: ProcessingEnvironment
-) : XMessager {
-    override fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+) : XMessager() {
+    override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
         val javacElement = (element as? JavacElement)?.element
         processingEnv.messager.printMessage(
             kind,
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
index dc9a6be..173ac32 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
@@ -77,16 +77,26 @@
         }
     }
 
-    override fun boxed(): XType {
-        return if (typeMirror.kind.isPrimitive) {
-            env.wrap(
-                typeMirror = env.typeUtils.boxedClass(MoreTypes.asPrimitiveType(typeMirror))
-                    .asType(),
-                kotlinType = kotlinType,
-                elementNullability = XNullability.NULLABLE
-            )
-        } else {
-            this
+    override fun boxed(): JavacType {
+        return when {
+            typeMirror.kind.isPrimitive -> {
+                env.wrap(
+                    typeMirror = env.typeUtils.boxedClass(MoreTypes.asPrimitiveType(typeMirror))
+                        .asType(),
+                    kotlinType = kotlinType,
+                    elementNullability = XNullability.NULLABLE
+                )
+            }
+            typeMirror.kind == TypeKind.VOID -> {
+                env.wrap(
+                    typeMirror = env.elementUtils.getTypeElement("java.lang.Void").asType(),
+                    kotlinType = kotlinType,
+                    elementNullability = XNullability.NULLABLE
+                )
+            }
+            else -> {
+                this
+            }
         }
     }
 
@@ -134,6 +144,32 @@
         return MoreTypes.isType(typeMirror)
     }
 
+    /**
+     * Create a copy of this type with the given nullability.
+     * This method is not called if the nullability of the type is already equal to the given
+     * nullability.
+     */
+    protected abstract fun copyWithNullability(nullability: XNullability): JavacType
+
+    final override fun makeNullable(): JavacType {
+        if (nullability == XNullability.NULLABLE) {
+            return this
+        }
+        if (typeMirror.kind.isPrimitive || typeMirror.kind == TypeKind.VOID) {
+            return boxed().makeNullable()
+        }
+        return copyWithNullability(XNullability.NULLABLE)
+    }
+
+    final override fun makeNonNullable(): JavacType {
+        if (nullability == XNullability.NONNULL) {
+            return this
+        }
+        // unlike makeNullable, we don't try to degrade to primitives here because it is valid for
+        // a boxed primitive to be marked as non-null.
+        return copyWithNullability(XNullability.NONNULL)
+    }
+
     companion object {
         private val BOXED_INT = TypeName.INT.box()
         private val BOXED_LONG = TypeName.LONG.box()
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
index f4a48bd..b7ec701 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
@@ -18,14 +18,17 @@
 
 import com.google.devtools.ksp.symbol.KSAnnotated
 
-internal fun KSAnnotated.hasJvmStaticAnnotation() = annotations.any {
-    it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmStatic"
+private fun KSAnnotated.hasAnnotationWithQName(qName: String) = annotations.any {
+    try {
+        it.annotationType.resolve().declaration.qualifiedName?.asString() == qName
+    } catch (illegal: IllegalStateException) {
+        // see: https://github.com/google/ksp/issues/173
+        false
+    }
 }
 
-internal fun KSAnnotated.hasJvmFieldAnnotation() = annotations.any {
-    it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmField"
-}
+internal fun KSAnnotated.hasJvmStaticAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmStatic")
 
-internal fun KSAnnotated.hasJvmDefaultAnnotation() = annotations.any {
-    it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmDefault"
-}
+internal fun KSAnnotated.hasJvmFieldAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmField")
+
+internal fun KSAnnotated.hasJvmDefaultAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmDefault")
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
index ba27191..6c230d8 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
@@ -26,6 +26,11 @@
  * Returns the type of a property as if it is member of the given [ksType].
  */
 internal fun KSPropertyDeclaration.typeAsMemberOf(resolver: Resolver, ksType: KSType): KSType {
+    if (isStatic()) {
+        // calling as member with a static would throw as it might be a member of the companion
+        // object
+        return type.resolve()
+    }
     return resolver.asMemberOf(
         property = this,
         containing = ksType
@@ -37,6 +42,11 @@
     functionDeclaration: KSFunctionDeclaration,
     ksType: KSType
 ): KSType {
+    if (functionDeclaration.isStatic()) {
+        // calling as member with a static would throw as it might be a member of the companion
+        // object
+        return type.resolve()
+    }
     val asMember = resolver.asMemberOf(
         function = functionDeclaration,
         containing = ksType
@@ -51,8 +61,15 @@
     resolver: Resolver,
     ksType: KSType
 ): KSType {
-    return resolver.asMemberOf(
-        function = this,
-        containing = ksType
-    ).returnType ?: returnType?.resolve() ?: error("cannot find return type for $this")
+    val returnType = if (isStatic()) {
+        // calling as member with a static would throw as it might be a member of the companion
+        // object
+        returnType?.resolve()
+    } else {
+        resolver.asMemberOf(
+            function = this,
+            containing = ksType
+        ).returnType
+    }
+    return returnType ?: error("cannot find return type for $this")
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
index 60beee8..206f312 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
@@ -18,6 +18,7 @@
 
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.Modifier
 
 /**
  * Finds the class that contains this declaration and throws [IllegalStateException] if it cannot
@@ -49,3 +50,7 @@
     }
     return parent as? KSClassDeclaration
 }
+
+internal fun KSDeclaration.isStatic(): Boolean {
+    return modifiers.contains(Modifier.JAVA_STATIC) || hasJvmStaticAnnotation()
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 31b2ad9..82f56d6 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -16,15 +16,11 @@
 
 package androidx.room.compiler.processing.ksp
 
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.javac.kotlin.typeNameFromJvmSignature
 import androidx.room.compiler.processing.tryBox
 import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.processing.Resolver
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeVariableName
-import com.squareup.javapoet.WildcardTypeName
 import com.google.devtools.ksp.symbol.KSDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeArgument
@@ -33,6 +29,11 @@
 import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Variance
 import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeVariableName
+import com.squareup.javapoet.WildcardTypeName
 
 internal const val ERROR_PACKAGE_NAME = "androidx.room.compiler.processing.kotlin.error"
 
@@ -99,7 +100,7 @@
                 TypeVariableName.get(param.name.asString(), type.typeName(resolver).tryBox())
             }
         }
-        else -> type.typeName(resolver)
+        else -> type.typeName(resolver).tryBox()
     }
 }
 
@@ -152,4 +153,10 @@
     return this.resolve().declaration is KSTypeParameter
 }
 
-fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
\ No newline at end of file
+fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
+
+internal fun KSType.withNullability(nullability: XNullability) = when (nullability) {
+    XNullability.NULLABLE -> makeNullable()
+    XNullability.NONNULL -> makeNotNullable()
+    else -> throw IllegalArgumentException("Cannot set KSType nullability to platform")
+}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt
index edfd964..b62e6ad 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt
@@ -19,6 +19,7 @@
 import androidx.room.compiler.processing.XAnnotationBox
 import androidx.room.compiler.processing.XType
 import com.google.devtools.ksp.symbol.KSAnnotation
+import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import java.lang.reflect.Proxy
 
@@ -29,7 +30,7 @@
     private val annotation: KSAnnotation
 ) : XAnnotationBox<T> {
     override fun getAsType(methodName: String): XType? {
-        val value = getFieldValue<KSType>(methodName)
+        val value = getFieldValue(methodName, KSType::class.java)
         return value?.let {
             env.wrap(
                 ksType = it,
@@ -39,8 +40,8 @@
     }
 
     override fun getAsTypeList(methodName: String): List<XType> {
-        val values = getFieldValue<List<KSType>>(methodName) ?: return emptyList()
-        return values.map {
+        val values = getFieldValue(methodName, Array::class.java) ?: return emptyList()
+        return values.filterIsInstance<KSType>().map {
             env.wrap(
                 ksType = it,
                 allowPrimitives = true
@@ -49,7 +50,17 @@
     }
 
     override fun <R : Annotation> getAsAnnotationBox(methodName: String): XAnnotationBox<R> {
-        val value = getFieldValue<KSAnnotation>(methodName) ?: error("cannot get annotation")
+        val value = getFieldValue(methodName, KSAnnotation::class.java)
+        @Suppress("FoldInitializerAndIfToElvis")
+        if (value == null) {
+            // see https://github.com/google/ksp/issues/53
+            return KspReflectiveAnnotationBox.createFromDefaultValue(
+                env = env,
+                annotationClass = annotationClass,
+                methodName = methodName
+            )
+        }
+
         val annotationType = annotationClass.methods.first {
             it.name == methodName
         }.returnType as Class<R>
@@ -60,22 +71,37 @@
         )
     }
 
-    private inline fun <reified R> getFieldValue(methodName: String): R? {
-        val value = annotation.arguments.firstOrNull {
+    @Suppress("SyntheticAccessor")
+    private fun <R : Any> getFieldValue(
+        methodName: String,
+        returnType: Class<R>
+    ): R? {
+        val methodValue = annotation.arguments.firstOrNull {
             it.name?.asString() == methodName
-        }?.value ?: return null
-        return value as R?
+        }?.value
+        return methodValue?.readAs(returnType)
     }
 
     override fun <R : Annotation> getAsAnnotationBoxArray(
         methodName: String
     ): Array<XAnnotationBox<R>> {
-        val values = getFieldValue<ArrayList<*>>(methodName) ?: return emptyArray()
+        val values = getFieldValue(methodName, Array::class.java) ?: return emptyArray()
         val annotationType = annotationClass.methods.first {
             it.name == methodName
         }.returnType.componentType as Class<R>
+        if (values.isEmpty()) {
+            // KSP is unable to read defaults and returns empty array in that case.
+            // Subsequently, we don't know if developer set it to empty array intentionally or
+            // left it to default.
+            // we error on the side of default
+            return KspReflectiveAnnotationBox.createFromDefaultValues(
+                env = env,
+                annotationClass = annotationClass,
+                methodName = methodName
+            )
+        }
         return values.map {
-            KspAnnotationBox<R>(
+            KspAnnotationBox(
                 env = env,
                 annotationClass = annotationType,
                 annotation = it as KSAnnotation
@@ -84,24 +110,52 @@
     }
 
     private val valueProxy: T = Proxy.newProxyInstance(
-        KspAnnotationBox::class.java.classLoader,
+        annotationClass.classLoader,
         arrayOf(annotationClass)
     ) { _, method, _ ->
-        val fieldValue = getFieldValue(method.name) ?: method.defaultValue
-        // java gives arrays, kotlin gives array list (sometimes?) so fix it up
-        when {
-            fieldValue == null -> null
-            method.returnType.isArray && (fieldValue is ArrayList<*>) -> {
-                val componentType = method.returnType.componentType!!
-                val result =
-                    java.lang.reflect.Array.newInstance(componentType, fieldValue.size) as Array<*>
-                fieldValue.toArray(result)
-                result
-            }
-            else -> fieldValue
-        }
+        getFieldValue(method.name, method.returnType) ?: method.defaultValue
     } as T
 
     override val value: T
         get() = valueProxy
 }
+
+@Suppress("UNCHECKED_CAST")
+private fun <R> Any.readAs(returnType: Class<R>): R? {
+    return when {
+        returnType.isArray -> {
+            val values = when (this) {
+                is List<*> -> {
+                    // KSP might return list for arrays. convert it back.
+                    this.mapNotNull {
+                        it?.readAs(returnType.componentType)
+                    }
+                }
+                is Array<*> -> mapNotNull { it?.readAs(returnType.componentType) }
+                else -> error("unexpected type for array: $this / ${this::class.java}")
+            }
+            val resultArray = java.lang.reflect.Array.newInstance(
+                returnType.componentType,
+                values.size
+            ) as Array<Any?>
+            values.forEachIndexed { index, value ->
+                resultArray[index] = value
+            }
+            resultArray
+        }
+        returnType.isEnum -> {
+            this.readAsEnum(returnType)
+        }
+        else -> this
+    } as R?
+}
+
+private fun <R> Any.readAsEnum(enumClass: Class<R>): R? {
+    val ksType = this as? KSType ?: return null
+    val classDeclaration = ksType.declaration as? KSClassDeclaration ?: return null
+    val enumValue = classDeclaration.simpleName.asString()
+    // get the instance from the valueOf function.
+    @Suppress("UNCHECKED_CAST", "BanUncheckedReflection")
+    return enumClass.getDeclaredMethod("valueOf", String::class.java)
+        .invoke(null, enumValue) as R?
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
index 7831603..6b9912e 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
@@ -56,6 +56,13 @@
                 allowPrimitives = false
             )
         }
+
+        override fun copyWithNullability(nullability: XNullability): BoxedArray {
+            return BoxedArray(
+                env = env,
+                ksType = ksType.withNullability(nullability)
+            )
+        }
     }
 
     /**
@@ -67,7 +74,15 @@
         override val componentType: KspType
     ) : KspArrayType(
         env, ksType
-    )
+    ) {
+        override fun copyWithNullability(nullability: XNullability): PrimitiveArray {
+            return PrimitiveArray(
+                env = env,
+                ksType = ksType.withNullability(nullability),
+                componentType = componentType
+            )
+        }
+    }
 
     /**
      * Factory class to create instances of [KspArrayType].
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
index e34f3ef..0932efc 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.room.compiler.processing.XDeclaredType
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.tryBox
 import com.google.devtools.ksp.symbol.KSType
@@ -38,7 +39,14 @@
         }
     }
 
-    override fun boxed(): XType {
+    override fun boxed(): KspDeclaredType {
         return this
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspType {
+        return KspDeclaredType(
+            env = env,
+            ksType = ksType.withNullability(nullability)
+        )
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index c1570e1..0ebc228 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -23,6 +23,7 @@
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.Modifier
 
 internal abstract class KspExecutableElement(
     env: KspProcessingEnv,
@@ -59,8 +60,11 @@
     }
 
     override fun isVarArgs(): Boolean {
-        return declaration.parameters.any {
-            it.isVararg
-        }
+        // in java, only the last argument can be a vararg so for suspend functions, it is never
+        // a vararg function. this would change if room generated kotlin code
+        return !declaration.modifiers.contains(Modifier.SUSPEND) &&
+            declaration.parameters.any {
+                it.isVararg
+            }
     }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
index 629d421..fcfa5de 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
@@ -20,7 +20,6 @@
 import androidx.room.compiler.processing.XDeclaredType
 import androidx.room.compiler.processing.XEquality
 import androidx.room.compiler.processing.XExecutableParameterElement
-import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.METHOD_PARAMETER
 import com.google.devtools.ksp.symbol.KSValueParameter
 
@@ -38,7 +37,7 @@
     override val name: String
         get() = parameter.name?.asString() ?: "_no_param_name"
 
-    override val type: XType by lazy {
+    override val type: KspType by lazy {
         parameter.typeAsMemberOf(
             resolver = env.resolver,
             functionDeclaration = method.declaration,
@@ -51,7 +50,7 @@
         }
     }
 
-    override fun asMemberOf(other: XDeclaredType): XType {
+    override fun asMemberOf(other: XDeclaredType): KspType {
         if (method.containing.type.isSameType(other)) {
             return type
         }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
index ea0aa93..aa1add9 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.room.compiler.processing.XHasModifiers
+import com.google.devtools.ksp.getVisibility
 import com.google.devtools.ksp.isOpen
 import com.google.devtools.ksp.isPrivate
 import com.google.devtools.ksp.isProtected
@@ -28,6 +29,7 @@
 import com.google.devtools.ksp.symbol.KSPropertyDeclaration
 import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Origin
+import com.google.devtools.ksp.symbol.Visibility
 
 /**
  * Implementation of [XHasModifiers] for ksp declarations.
@@ -36,7 +38,9 @@
     protected val declaration: KSDeclaration
 ) : XHasModifiers {
     override fun isPublic(): Boolean {
-        return declaration.isPublic()
+        // internals are public from java but KSP's declaration.isPublic excludes them.
+        return declaration.getVisibility() == Visibility.INTERNAL ||
+            declaration.getVisibility() == Visibility.PUBLIC
     }
 
     override fun isProtected(): Boolean {
@@ -52,8 +56,7 @@
     }
 
     override fun isStatic(): Boolean {
-        return declaration.modifiers.contains(Modifier.JAVA_STATIC) ||
-            declaration.hasJvmStaticAnnotation()
+        return declaration.isStatic()
     }
 
     override fun isTransient(): Boolean {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
index 557cfa3..1497d90 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
@@ -23,8 +23,8 @@
 
 internal class KspMessager(
     private val logger: KSPLogger
-) : XMessager {
-    override fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+) : XMessager() {
+    override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
         val ksNode = (element as? KspElement)?.declaration
         when (kind) {
             Diagnostic.Kind.ERROR -> logger.error(msg, ksNode)
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
index 48841e32..359f413 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
@@ -42,13 +42,7 @@
 
     @OptIn(KspExperimental::class)
     override val name: String by lazy {
-        try {
-            env.resolver.getJvmName(declaration)
-        } catch (ignored: ClassCastException) {
-            // TODO remove this catch once that issue is fixed.
-            // workaround for https://github.com/google/ksp/issues/164
-            declaration.simpleName.asString()
-        }
+        env.resolver.safeGetJvmName(declaration)
     }
 
     override val executableType: XMethodType by lazy {
@@ -104,9 +98,11 @@
     ) {
         override val returnType: XType by lazy {
             env.wrap(
-                checkNotNull(declaration.returnType) {
-                    "return type on a method declaration cannot be null"
-                }
+                ksType = declaration.returnTypeAsMemberOf(
+                    resolver = env.resolver,
+                    ksType = containing.type.ksType
+                ),
+                originatingReference = checkNotNull(declaration.returnType)
             )
         }
         override fun isSuspendFunction() = false
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
index 0ce3d78..4ce33b0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
@@ -44,6 +44,15 @@
         }
     }
 
+    /**
+     * Creates a MethodType where variance is inherited for java code generation.
+     *
+     * see [OverrideVarianceResolver] for details.
+     */
+    fun inheritVarianceForOverride(): XMethodType {
+        return OverrideVarianceResolver(env, this).resolve()
+    }
+
     private class KspNormalMethodType(
         env: KspProcessingEnv,
         origin: KspMethodElement,
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
index 29fed31..c006611 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
@@ -16,7 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.tryUnbox
 import com.google.devtools.ksp.symbol.KSType
 import com.squareup.javapoet.TypeName
@@ -35,10 +35,27 @@
     override val typeName: TypeName
         get() = ksType.typeName(env.resolver).tryUnbox()
 
-    override fun boxed(): XType {
+    override fun boxed(): KspType {
         return env.wrap(
             ksType = ksType,
             allowPrimitives = false
         )
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspType {
+        return when (nullability) {
+            XNullability.NONNULL -> {
+                this
+            }
+            XNullability.NULLABLE -> {
+                // primitive types cannot be nullable hence we box them.
+                boxed().makeNullable()
+            }
+            else -> {
+                // this should actually never happens as the only time this is called is from
+                // make nullable-make nonnull but we have this error here for completeness.
+                error("cannot set nullability to unknown in KSP")
+            }
+        }
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt
new file mode 100644
index 0000000..3bdd7ae
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.ksp
+
+import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.processing.XAnnotationBox
+import androidx.room.compiler.processing.XType
+
+/**
+ * KSP sometimes cannot read default values in annotations. This reflective implementation
+ * handles those cases.
+ * see: https://github.com/google/ksp/issues/53
+ */
+internal class KspReflectiveAnnotationBox<T : Annotation> @VisibleForTesting constructor(
+    private val env: KspProcessingEnv,
+    private val annotationClass: Class<T>,
+    private val annotation: T
+) : XAnnotationBox<T> {
+    override val value: T = annotation
+
+    override fun getAsType(methodName: String): XType? {
+        val value = getFieldValue<Class<*>>(methodName) ?: return null
+        return env.findType(value.kotlin)
+    }
+
+    override fun getAsTypeList(methodName: String): List<XType> {
+        val values = getFieldValue<Array<*>>(methodName)
+        return values?.filterIsInstance<Class<*>>()?.mapNotNull {
+            env.findType(it.kotlin)
+        } ?: emptyList()
+    }
+
+    override fun <T : Annotation> getAsAnnotationBox(methodName: String): XAnnotationBox<T> {
+        return createFromDefaultValue(
+            env = env,
+            annotationClass = annotationClass,
+            methodName = methodName
+        )
+    }
+
+    @Suppress("UNCHECKED_CAST", "BanUncheckedReflection")
+    override fun <T : Annotation> getAsAnnotationBoxArray(
+        methodName: String
+    ): Array<XAnnotationBox<T>> {
+        val method = annotationClass.methods.firstOrNull {
+            it.name == methodName
+        } ?: error("$annotationClass does not contain $methodName")
+        val values = method.invoke(annotation) as? Array<T> ?: return emptyArray()
+        return values.map {
+            KspReflectiveAnnotationBox(
+                env = env,
+                annotationClass = method.returnType.componentType as Class<T>,
+                annotation = it
+            )
+        }.toTypedArray()
+    }
+
+    @Suppress("UNCHECKED_CAST", "BanUncheckedReflection")
+    private fun <R : Any> getFieldValue(methodName: String): R? {
+        val value = annotationClass.methods.firstOrNull {
+            it.name == methodName
+        }?.invoke(annotation) ?: return null
+        return value as R?
+    }
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        fun <R : Annotation> createFromDefaultValue(
+            env: KspProcessingEnv,
+            annotationClass: Class<*>,
+            methodName: String
+        ): KspReflectiveAnnotationBox<R> {
+            val method = annotationClass.methods.firstOrNull {
+                it.name == methodName
+            } ?: error("$annotationClass does not contain $methodName")
+            val defaultValue = method.defaultValue
+                ?: error("$annotationClass.$method does not have a default value and is not set")
+            return KspReflectiveAnnotationBox(
+                env = env,
+                annotationClass = method.returnType as Class<R>,
+                annotation = defaultValue as R
+            )
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        fun <R : Annotation> createFromDefaultValues(
+            env: KspProcessingEnv,
+            annotationClass: Class<*>,
+            methodName: String
+        ): Array<XAnnotationBox<R>> {
+            val method = annotationClass.methods.firstOrNull {
+                it.name == methodName
+            } ?: error("$annotationClass does not contain $methodName")
+            check(method.returnType.isArray) {
+                "expected ${method.returnType} to be an array. $method"
+            }
+            val defaultValue = method.defaultValue
+                ?: error("$annotationClass.$method does not have a default value and is not set")
+            val values: Array<R> = defaultValue as Array<R>
+            return values.map {
+                KspReflectiveAnnotationBox(
+                    env = env,
+                    annotationClass = method.returnType.componentType as Class<R>,
+                    annotation = it
+                )
+            }.toTypedArray()
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index cb03dab..1292542 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -156,4 +156,27 @@
     override fun isEnum(): Boolean {
         return (ksType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS
     }
+
+    abstract override fun boxed(): KspType
+
+    /**
+     * Create a copy of this type with the given nullability.
+     * This method is not called if the nullability of the type is already equal to the given
+     * nullability.
+     */
+    protected abstract fun copyWithNullability(nullability: XNullability): KspType
+
+    final override fun makeNullable(): KspType {
+        if (nullability == XNullability.NULLABLE) {
+            return this
+        }
+        return copyWithNullability(XNullability.NULLABLE)
+    }
+
+    final override fun makeNonNullable(): KspType {
+        if (nullability == XNullability.NONNULL) {
+            return this
+        }
+        return copyWithNullability(XNullability.NONNULL)
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index 36a3e12..0690474 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -16,10 +16,11 @@
 
 package androidx.room.compiler.processing.ksp
 
-import androidx.room.compiler.processing.XType
-import com.squareup.javapoet.TypeName
+import androidx.room.compiler.processing.XNullability
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.squareup.javapoet.TypeName
 
 /**
  * The typeName for type arguments requires the type parameter, hence we have a special type
@@ -37,7 +38,23 @@
         typeArg.typeName(typeParam, env.resolver)
     }
 
-    override fun boxed(): XType {
+    override fun boxed(): KspTypeArgumentType {
         return this
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspTypeArgumentType {
+        return KspTypeArgumentType(
+            env = env,
+            typeParam = typeParam,
+            typeArg = DelegatingTypeArg(
+                original = typeArg,
+                type = typeArg.requireType().withNullability(nullability).createTypeReference()
+            )
+        )
+    }
+
+    private class DelegatingTypeArg(
+        val original: KSTypeArgument,
+        override val type: KSTypeReference
+    ) : KSTypeArgument by original
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index eb88523..22993b2 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -106,8 +106,14 @@
     }
 
     private val _declaredPropertyFields by lazy {
-        declaration
-            .getDeclaredProperties()
+        val declaredProperties = declaration.getDeclaredProperties()
+        val companionProperties = declaration
+            .findCompanionObject()
+            ?.getDeclaredProperties()
+            ?.filter {
+                it.isStatic()
+            }.orEmpty()
+        (declaredProperties + companionProperties)
             .map {
                 KspFieldElement(
                     env = env,
@@ -221,7 +227,15 @@
     }
 
     private val _declaredMethods by lazy {
-        val myMethods = declaration.getDeclaredFunctions().asSequence()
+        val instanceMethods = declaration.getDeclaredFunctions().asSequence()
+        val companionMethods = declaration.findCompanionObject()
+            ?.getDeclaredFunctions()
+            ?.asSequence()
+            ?.filter {
+                it.isStatic()
+            }
+            ?: emptySequence()
+        val declaredMethods = (instanceMethods + companionMethods)
             .filterNot {
                 // filter out constructors
                 it.simpleName.asString() == name
@@ -238,15 +252,7 @@
                     declaration = it
                 )
             }.toList()
-        val companionMethods = declaration.findCompanionObject()
-            ?.let {
-                env.wrapClassDeclaration(it)
-            }?.getDeclaredMethods()
-            ?.filter {
-                it.isStatic()
-            } ?: emptyList()
-
-        myMethods + syntheticGetterSetterMethods + companionMethods
+        declaredMethods + syntheticGetterSetterMethods
     }
 
     override fun getDeclaredMethods(): List<XMethodElement> {
@@ -311,4 +317,8 @@
         .filter {
             it.simpleName == this.simpleName
         }
+
+    override fun toString(): String {
+        return declaration.toString()
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
index 734f3b1a..06a1983 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
@@ -75,7 +75,7 @@
         mapping["java.lang.Boolean"] = "kotlin.Boolean"
         // collections. default to mutable ones since java types are always mutable
         mapping["java.util.Iterator"] = "kotlin.collections.MutableIterator"
-        mapping["java.util.Iterable"] = "kotlin.collections.Iterable"
+        mapping["java.lang.Iterable"] = "kotlin.collections.Iterable"
         mapping["java.util.Collection"] = "kotlin.collections.MutableCollection"
         mapping["java.util.Set"] = "kotlin.collections.MutableSet"
         mapping["java.util.List"] = "kotlin.collections.MutableList"
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
index 2bf6728..cc5a601 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
@@ -16,7 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XNullability
 import com.google.devtools.ksp.symbol.KSType
 import com.squareup.javapoet.TypeName
 
@@ -30,16 +30,16 @@
 internal class KspVoidType(
     env: KspProcessingEnv,
     ksType: KSType,
-    private val boxed: Boolean
+    val boxed: Boolean
 ) : KspType(env, ksType) {
     override val typeName: TypeName
-        get() = if (boxed) {
+        get() = if (boxed || nullability == XNullability.NULLABLE) {
             TypeName.VOID.box()
         } else {
             TypeName.VOID
         }
 
-    override fun boxed(): XType {
+    override fun boxed(): KspType {
         return if (boxed) {
             this
         } else {
@@ -50,4 +50,12 @@
             )
         }
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspType {
+        return KspVoidType(
+            env = env,
+            ksType = ksType.withNullability(nullability),
+            boxed = boxed || nullability == XNullability.NULLABLE
+        )
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt
new file mode 100644
index 0000000..d4f3e0e
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XMethodType
+import androidx.room.compiler.processing.XType
+import com.google.devtools.ksp.closestClassDeclaration
+import com.google.devtools.ksp.isOpen
+import com.google.devtools.ksp.symbol.ClassKind
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeArgument
+import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Variance
+import com.squareup.javapoet.TypeVariableName
+
+/**
+ * When kotlin generates java code, it has some interesting rules on how variance is handled.
+ *
+ * https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#variant-generics
+ *
+ * This helper class applies that to [KspMethodType].
+ *
+ * Note that, this is only relevant when Room tries to generate overrides. For regular type
+ * operations, we prefer the variance declared in Kotlin source.
+ */
+internal class OverrideVarianceResolver(
+    private val env: KspProcessingEnv,
+    private val methodType: KspMethodType
+) {
+    fun resolve(): XMethodType {
+        val overideeElm = methodType.origin.findOverridee()
+        return ResolvedMethodType(
+            // kotlin does not touch return type
+            returnType = methodType.returnType,
+            parameterTypes = methodType.parameterTypes.mapIndexed { index, xType ->
+                xType.maybeInheritVariance(overideeElm?.parameterTypes?.getOrNull(index))
+            },
+            typeVariableNames = methodType.typeVariableNames
+        )
+    }
+
+    private fun XType.maybeInheritVariance(
+        overridee: XType?
+    ): XType {
+        return if (this is KspType) {
+            this.inheritVariance(overridee as? KspType)
+        } else {
+            this
+        }
+    }
+
+    private fun KspType.inheritVariance(overridee: KspType?): KspType {
+        return env.wrap(
+            ksType = ksType.inheritVariance(overridee?.ksType),
+            allowPrimitives = this is KspPrimitiveType || (this is KspVoidType && !this.boxed)
+        )
+    }
+
+    /**
+     * Finds the method type for the method element that was overridden by this method element.
+     */
+    private fun KspMethodElement.findOverridee(): KspMethodType? {
+        // now find out if this is overriding a method
+        val funDeclaration = declaration
+        val declaredIn = funDeclaration.closestClassDeclaration() ?: return null
+        if (declaredIn == containing.declaration) {
+            // if declared in the same class, skip
+            return null
+        }
+        // it is declared in a super type, get that
+        val overrideeElm = KspMethodElement.create(
+            env = env,
+            containing = env.wrapClassDeclaration(declaredIn),
+            declaration = funDeclaration.findOverridee() ?: funDeclaration
+        )
+        val containing = overrideeElm.enclosingTypeElement.type as? KspDeclaredType ?: return null
+        return KspMethodType.create(
+            env = env,
+            origin = overrideeElm,
+            containing = containing
+        )
+    }
+
+    /**
+     * Update the variance of the arguments of this type based on the types declaration.
+     *
+     * For instance, in List<Foo>, it actually inherits the `out` variance from `List`.
+     */
+    private fun KSType.inheritVariance(
+        overridee: KSType?
+    ): KSType {
+        if (arguments.isEmpty()) return this
+        // need to swap arguments with the variance from declaration
+        val newArguments = arguments.mapIndexed { index, typeArg ->
+            val param = declaration.typeParameters.getOrNull(index)
+            val overrideeArg = overridee?.arguments?.getOrNull(index)
+            typeArg.inheritVariance(overrideeArg, param)
+        }
+        return this.replace(newArguments)
+    }
+
+    private fun KSTypeReference.inheritVariance(
+        overridee: KSTypeReference?
+    ): KSTypeReference {
+        return resolve()
+            .inheritVariance(overridee = overridee?.resolve())
+            .createTypeReference()
+    }
+
+    private fun KSTypeArgument.inheritVariance(
+        overridee: KSTypeArgument?,
+        param: KSTypeParameter?
+    ): KSTypeArgument {
+        if (param == null) {
+            return this
+        }
+        val myTypeRef = type ?: return this
+
+        if (variance != Variance.INVARIANT) {
+            return env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee?.type),
+                variance = variance
+            )
+        }
+        if (overridee != null) {
+            // get it from overridee
+            return env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee.type),
+                variance = if (overridee.variance == Variance.STAR) {
+                    Variance.COVARIANT
+                } else {
+                    overridee.variance
+                }
+            )
+        }
+        // Now we need to guess from this type. If the type is final, it does not inherit unless
+        // the parameter is CONTRAVARIANT (`in`).
+        val myType = myTypeRef.resolve()
+        val shouldInherit = param.variance == Variance.CONTRAVARIANT ||
+            when (val decl = myType.declaration) {
+                is KSClassDeclaration -> {
+                    decl.isOpen() ||
+                        decl.classKind == ClassKind.ENUM_CLASS ||
+                        decl.classKind == ClassKind.OBJECT
+                }
+                else -> true
+            }
+        return if (shouldInherit) {
+            env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee = null),
+                variance = param.variance
+            )
+        } else {
+            env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee = null),
+                variance = variance
+            )
+        }
+    }
+
+    /**
+     * [XMethodType] implementation where variance of types are resolved.
+     */
+    private class ResolvedMethodType(
+        override val returnType: XType,
+        override val parameterTypes: List<XType>,
+        override val typeVariableNames: List<TypeVariableName>
+    ) : XMethodType
+}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
index 4901b29e..876eb4a 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
@@ -17,11 +17,14 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.room.compiler.processing.XExecutableElement
+import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
-import com.google.devtools.ksp.closestClassDeclaration
-import com.google.devtools.ksp.getAllSuperTypes
+import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSPropertyAccessor
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
 
 internal fun Resolver.findClass(qName: String) = getClassDeclarationByName(
     getKSNameFromString(qName)
@@ -42,8 +45,8 @@
 }
 
 internal fun Resolver.overrides(
-    overriderElement: XExecutableElement,
-    overrideeElement: XExecutableElement
+    overriderElement: XMethodElement,
+    overrideeElement: XMethodElement
 ): Boolean {
     // in addition to functions declared in kotlin, we also synthesize getter/setter functions for
     // properties which means we cannot simply send the declaration to KSP for override check
@@ -55,16 +58,69 @@
     if (overriderElement.parameters.size != overrideeElement.parameters.size) {
         return false
     }
-    val ksOverrider = overriderElement.getDeclarationForOverride()
-    val ksOverridee = overrideeElement.getDeclarationForOverride()
-    if (!overrides(ksOverrider, ksOverridee)) {
+    // do a quick check on name before doing the more expensive operations
+    if (overriderElement.name != overrideeElement.name) {
         return false
     }
-    // TODO Workaround for https://github.com/google/ksp/issues/123
-    //  remove once that bug is fixed
-    val subClass = ksOverrider.closestClassDeclaration() ?: return false
-    val superClass = ksOverridee.closestClassDeclaration() ?: return false
-    return subClass.getAllSuperTypes().any {
-        it.declaration.closestClassDeclaration() == superClass
+    val ksOverrider = overriderElement.getDeclarationForOverride()
+    val ksOverridee = overrideeElement.getDeclarationForOverride()
+    if (overrides(ksOverrider, ksOverridee)) {
+        return true
+    }
+    // workaround for: https://github.com/google/ksp/issues/175
+    if (ksOverrider is KSFunctionDeclaration && ksOverridee is KSFunctionDeclaration) {
+        return ksOverrider.overrides(ksOverridee)
+    }
+    if (ksOverrider is KSPropertyDeclaration && ksOverridee is KSPropertyDeclaration) {
+        return ksOverrider.overrides(ksOverridee)
+    }
+    return false
+}
+
+private fun KSFunctionDeclaration.overrides(other: KSFunctionDeclaration): Boolean {
+    val overridee = try {
+        findOverridee()
+    } catch (ignored: ClassCastException) {
+        // workaround for https://github.com/google/ksp/issues/164
+        null
+    }
+    if (overridee == other) {
+        return true
+    }
+    return overridee?.overrides(other) ?: false
+}
+
+private fun KSPropertyDeclaration.overrides(other: KSPropertyDeclaration): Boolean {
+    val overridee = findOverridee()
+    if (overridee == other) {
+        return true
+    }
+    return overridee?.overrides(other) ?: false
+}
+
+@OptIn(KspExperimental::class)
+internal fun Resolver.safeGetJvmName(
+    declaration: KSFunctionDeclaration
+): String {
+    return try {
+        getJvmName(declaration)
+    } catch (ignored: ClassCastException) {
+        // TODO remove this catch once that issue is fixed.
+        // workaround for https://github.com/google/ksp/issues/164
+        return declaration.simpleName.asString()
+    }
+}
+
+@OptIn(KspExperimental::class)
+internal fun Resolver.safeGetJvmName(
+    accessor: KSPropertyAccessor,
+    fallback: () -> String
+): String {
+    return try {
+        getJvmName(accessor)
+    } catch (ignored: ClassCastException) {
+        // TODO remove this catch once that issue is fixed.
+        // workaround for https://github.com/google/ksp/issues/164
+        return fallback()
     }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
index 68121e3..f2b09cb 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
@@ -46,8 +46,17 @@
         filter = NO_USE_SITE
     ) {
 
-    override val name: String
-        get() = "_syntheticContinuation"
+    override val name: String by lazy {
+        // kotlin names this as pN where N is the # of arguments
+        // seems like kapt doesn't handle conflicts with declared arguments but we should
+        val desiredName = "p${containing.declaration.parameters.size}"
+
+        if (containing.declaration.parameters.none { it.name?.asString() == desiredName }) {
+            desiredName
+        } else {
+            "_syntheticContinuation"
+        }
+    }
 
     override val equalityItems: Array<out Any?> by lazy {
         arrayOf("continuation", containing)
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 868a69c..bf906fd 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -35,6 +35,7 @@
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import androidx.room.compiler.processing.ksp.KspTypeElement
 import androidx.room.compiler.processing.ksp.overrides
+import androidx.room.compiler.processing.ksp.safeGetJvmName
 import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.symbol.KSPropertyAccessor
 import java.util.Locale
@@ -121,15 +122,11 @@
         @OptIn(KspExperimental::class)
         override val name: String by lazy {
             field.declaration.getter?.let {
-                return@lazy env.resolver.getJvmName(it)
+                return@lazy env.resolver.safeGetJvmName(it) {
+                    computeGetterName(field.name)
+                }
             }
-            // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
-            val propName = field.name
-            if (propName.startsWith("is")) {
-                propName
-            } else {
-                "get${propName.capitalize(Locale.US)}"
-            }
+            computeGetterName(field.name)
         }
 
         override val returnType: XType by lazy {
@@ -150,6 +147,17 @@
                 field = field.copyTo(newContainer)
             )
         }
+
+        companion object {
+            private fun computeGetterName(propName: String): String {
+                // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
+                return if (propName.startsWith("is")) {
+                    propName
+                } else {
+                    "get${propName.capitalize(Locale.US)}"
+                }
+            }
+        }
     }
 
     internal class Setter(
@@ -176,15 +184,11 @@
         @OptIn(KspExperimental::class)
         override val name: String by lazy {
             field.declaration.setter?.let {
-                return@lazy env.resolver.getJvmName(it)
+                return@lazy env.resolver.safeGetJvmName(it) {
+                    computeSetterName(field.name)
+                }
             }
-            // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
-            val propName = field.name
-            if (propName.startsWith("is")) {
-                "set${propName.substring(2)}"
-            } else {
-                "set${propName.capitalize(Locale.US)}"
-            }
+            computeSetterName(field.name)
         }
 
         override val returnType: XType by lazy {
@@ -238,5 +242,16 @@
                 return "method parameter"
             }
         }
+
+        companion object {
+            private fun computeSetterName(propName: String): String {
+                // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
+                return if (propName.startsWith("is")) {
+                    "set${propName.substring(2)}"
+                } else {
+                    "set${propName.capitalize(Locale.US)}"
+                }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
index e0f2844..b31aa76 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
@@ -16,10 +16,13 @@
 
 package androidx.room.compiler.processing
 
-import androidx.room.compiler.processing.javac.JavacProcessingEnv
+import androidx.room.compiler.processing.javac.JavacMethodElement
+import androidx.room.compiler.processing.javac.JavacTypeElement
 import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.javaTypeUtils
 import androidx.room.compiler.processing.util.runKaptTest
-import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.compiler.processing.util.runProcessorTest
 import com.google.auto.common.MoreTypes
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.MethodSpec
@@ -27,7 +30,6 @@
 import javax.lang.model.element.ExecutableElement
 import javax.lang.model.element.Modifier
 import javax.lang.model.type.DeclaredType
-import javax.lang.model.util.ElementFilter
 import javax.lang.model.util.Types
 
 class MethodSpecHelperTest {
@@ -101,10 +103,35 @@
                     return 3;
                 }
 
+                open fun boxedLongArrayReturn(): Array<Long> {
+                    TODO();
+                }
+
+                open fun boxedIntArrayReturn(): Array<Int> {
+                    TODO();
+                }
+
+                protected open fun listArg(r:List<String>) {
+                }
+
+                open suspend fun suspendUnitFun() {
+                }
+
+                protected open suspend fun suspendBasic(p0:Int):String {
+                    TODO()
+                }
+
+                protected open suspend fun suspendVarArg(p0:Int, vararg p1:String):Long {
+                    TODO()
+                }
+
                 protected open fun <R> typeArgs(r:R): R {
                     return r;
                 }
 
+                internal open fun internalFun() {
+                }
+
                 @Throws(Exception::class)
                 protected open fun throwsException() {
                 }
@@ -114,47 +141,228 @@
         overridesCheck(source)
     }
 
-    private fun overridesCheck(source: Source) {
+    @Test
+    fun variance() {
+        // check our override impl matches javapoet
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface MyInterface<T> {
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            interface I1<in T>
+            interface I2<out T>
+            interface I3<T>
+            enum class Lang {
+               ES,
+               EN;
+            }
+            class Box<out T>(val value: T)
+
+            interface Base
+            class Derived : Base
+
+            interface Baz : MyInterface<String> {
+                fun boxDerived(value: Derived): Box<Derived> = Box(value)
+                fun unboxBase(box: Box<Base>): Base = box.value
+                fun unboxString(box: Box<String>): String = box.value
+                fun findByLanguages(langs: Set<Lang>): List<String>
+                fun f1(args : I1<String>): I1<String>
+                fun f2(args : I2<String>): I2<String>
+                fun f3(args : I3<String>): I3<String>
+                suspend fun s1(args : I1<String>): I1<String>
+                suspend fun s2(args : I2<String>): I2<String>
+                suspend fun s3(args : I3<String>): I3<String>
+                suspend fun s4(args : I1<String>): String
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun inheritedVariance_openType() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface MyInterface<T> {
+                fun receiveList(argsInParent : List<T>):Unit
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            open class Book(val id:Int)
+            interface Baz : MyInterface<Book> {
+                fun myList(args: List<Book>):Unit
+                override fun receiveList(argsInParent : List<Book>):Unit
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun inheritedVariance_finalType() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface MyInterface<T> {
+                fun receiveList(argsInParent : List<T>):Unit
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            interface Baz : MyInterface<String> {
+                fun myList(args: List<String>):Unit
+                override fun receiveList(argsInParent : List<String>):Unit
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source, ignoreInheritedMethods = true)
+    }
+
+    @Test
+    fun inheritedVariance_enumType() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            enum class EnumType {
+                FOO,
+                BAR;
+            }
+            interface MyInterface<T> {
+                fun receiveList(argsInParent : List<T>):Unit
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            interface Baz : MyInterface<EnumType> {
+                fun myList(args: List<EnumType>):Unit
+                override fun receiveList(argsInParent : List<EnumType>):Unit
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun inheritedVariance_multiLevel() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface GrandParent<T> {
+                fun receiveList(list : List<T>): Unit
+                suspend fun suspendReceiveList(list : List<T>): Unit
+                suspend fun suspendReturnList(): List<T>
+            }
+            interface Parent: GrandParent<Number> {
+            }
+            interface Baz : Parent {
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun primitiveOverrides() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar
+            data class LongFoo(val id: Long, val description: String)
+            /* Interface with generics only */
+            interface MyInterface<Key, Value> {
+                fun getItem(id: Key): Value?
+                //fun delete(id: Key)
+                //fun getFirstItemId(): Key
+            }
+            /* Interface with non-generics and generics */
+            interface Baz : MyInterface<Long, LongFoo> {
+                override fun getItem(id: Long): LongFoo?
+                //override fun delete(id: Long)
+                //fun insert(item: LongFoo)
+                //override fun getFirstItemId(): Long
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    private fun overridesCheck(source: Source, ignoreInheritedMethods: Boolean = false) {
         // first build golden image with Java processor so we can use JavaPoet's API
-        val golden = buildMethodsViaJavaPoet(source)
-        runKspTest(
-            sources = listOf(source),
-            succeed = true
+        val golden = buildMethodsViaJavaPoet(source, ignoreInheritedMethods)
+        runProcessorTest(
+            sources = listOf(source)
         ) { invocation ->
-            val element = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
-            element.getDeclaredMethods().filter {
-                // TODO b/171572318
-                !invocation.isKsp || it.name != "throwsException"
-            }.forEachIndexed { index, method ->
-                val subject = MethodSpecHelper.overridingWithFinalParams(
-                    method,
-                    element.type
-                ).build().toString()
-                assertThat(subject).isEqualTo(golden[index])
+            val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
+            methods.forEachIndexed { index, method ->
+
+                if (invocation.isKsp && method.name == "throwsException") {
+                    // TODO b/171572318
+                } else {
+                    val subject = MethodSpecHelper.overridingWithFinalParams(
+                        method,
+                        target.type
+                    ).build().toString()
+                    assertThat(subject).isEqualTo(golden[index])
+                }
             }
         }
     }
 
-    private fun buildMethodsViaJavaPoet(source: Source): List<String> {
+    private fun buildMethodsViaJavaPoet(
+        source: Source,
+        ignoreInheritedMethods: Boolean
+    ): List<String> {
         lateinit var result: List<String>
         runKaptTest(
-            sources = listOf(source),
-            succeed = true
-        ) {
-            val processingEnv = (it.processingEnv as JavacProcessingEnv)
-            val element = processingEnv.elementUtils.getTypeElement("foo.bar.Baz")
-            result = ElementFilter.methodsIn(element.enclosedElements)
+            sources = listOf(source)
+        ) { invocation ->
+            val (target, methods) = invocation.getOverrideTestTargets(
+                ignoreInheritedMethods
+            )
+            val element = (target as JavacTypeElement).element
+            result = methods
                 .map {
+                    (it as JavacMethodElement).element
+                }.map {
                     generateFromJavapoet(
                         it,
                         MoreTypes.asDeclared(element.asType()),
-                        processingEnv.typeUtils
+                        invocation.javaTypeUtils
                     ).build().toString()
                 }
         }
         return result
     }
 
+    /**
+     * Get test targets. There is an edge case where it is not possible to implement an interface
+     * in java, b/174313780. [ignoreInheritedMethods] helps avoid that case.
+     */
+    private fun XTestInvocation.getOverrideTestTargets(
+        ignoreInheritedMethods: Boolean
+    ): Pair<XTypeElement, List<XMethodElement>> {
+        val objectMethodNames = processingEnv
+            .requireTypeElement("java.lang.Object")
+            .getAllNonPrivateInstanceMethods()
+            .map {
+                it.name
+            }
+        val target = processingEnv.requireTypeElement("foo.bar.Baz")
+        val methods = if (ignoreInheritedMethods) {
+            target.getDeclaredMethods().filter { !it.isStatic() }
+        } else {
+            target.getAllNonPrivateInstanceMethods()
+        }
+        val selectedMethods = methods.filter {
+            it.isOverrideableIgnoringContainer()
+        }.filterNot {
+            it.name in objectMethodNames
+        }
+        return target to selectedMethods
+    }
+
     private fun generateFromJavapoet(
         method: ExecutableElement,
         owner: DeclaredType,
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
index 7a9a0ac..40cab24 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
@@ -16,6 +16,8 @@
 
 package androidx.room.compiler.processing
 
+import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults
+import androidx.room.compiler.processing.testcode.JavaEnum
 import androidx.room.compiler.processing.testcode.MainAnnotation
 import androidx.room.compiler.processing.testcode.OtherAnnotation
 import androidx.room.compiler.processing.util.Source
@@ -23,13 +25,15 @@
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.getParameter
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import com.squareup.javapoet.ClassName
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import java.util.LinkedHashMap
 
 @RunWith(JUnit4::class)
 class XAnnotationBoxTest {
@@ -44,7 +48,6 @@
             }
             """.trimIndent()
         )
-        // TODO add KSP once https://github.com/google/ksp/issues/96 is fixed.
         runProcessorTest(
             sources = listOf(source)
         ) {
@@ -83,7 +86,8 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        // re-enable after fixing b/175144186
+        runProcessorTestWithoutKsp(
             listOf(mySource)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -125,7 +129,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("Subject")
@@ -165,7 +169,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(mySource)
         ) { invocation ->
             val element = invocation.processingEnv.requireTypeElement("Subject")
@@ -225,7 +229,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("Subject")
 
             subject.getField("prop1").assertHasSuppressWithValue("onProp1")
@@ -276,7 +280,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("Subject")
             subject.getMethod("noAnnotations").let { method ->
                 method.assertDoesNotHaveAnnotation()
@@ -305,7 +309,7 @@
             )
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("Subject")
             subject.assertHasSuppressWithValue("onClass")
             val constructor = subject.getConstructors().single()
@@ -316,6 +320,76 @@
         }
     }
 
+    @Test
+    fun defaultValues() {
+        val kotlinSrc = Source.kotlin(
+            "KotlinClass.kt",
+            """
+            import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults
+            @JavaAnnotationWithDefaults
+            class KotlinClass
+            """.trimIndent()
+        )
+        val javaSrc = Source.java(
+            "JavaClass.java",
+            """
+            import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults;
+            @JavaAnnotationWithDefaults
+            class JavaClass {}
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
+            listOf("KotlinClass", "JavaClass")
+                .map {
+                    invocation.processingEnv.requireTypeElement(it)
+                }.forEach { typeElement ->
+                    val annotation =
+                        typeElement.toAnnotationBox(JavaAnnotationWithDefaults::class)
+                    checkNotNull(annotation)
+                    assertThat(annotation.value.intVal).isEqualTo(3)
+                    assertThat(annotation.value.stringArrayVal).isEqualTo(arrayOf("x", "y"))
+                    assertThat(annotation.value.stringVal).isEqualTo("foo")
+                    assertThat(
+                        annotation.getAsType("typeVal")?.rawType?.typeName
+                    ).isEqualTo(
+                        ClassName.get(HashMap::class.java)
+                    )
+                    assertThat(
+                        annotation.getAsTypeList("typeArrayVal").map {
+                            it.rawType.typeName
+                        }
+                    ).isEqualTo(
+                        listOf(ClassName.get(LinkedHashMap::class.java))
+                    )
+
+                    assertThat(
+                        annotation.value.enumVal
+                    ).isEqualTo(
+                        JavaEnum.DEFAULT
+                    )
+
+                    assertThat(
+                        annotation.value.enumArrayVal
+                    ).isEqualTo(
+                        arrayOf(JavaEnum.VAL1, JavaEnum.VAL2)
+                    )
+
+                    assertThat(
+                        annotation.getAsAnnotationBox<OtherAnnotation>("otherAnnotationVal")
+                            .value.value
+                    ).isEqualTo("def")
+
+                    assertThat(
+                        annotation
+                            .getAsAnnotationBoxArray<OtherAnnotation>("otherAnnotationArrayVal")
+                            .map {
+                                it.value.value
+                            }
+                    ).containsExactly("v1")
+                }
+        }
+    }
+
     // helper function to read what we need
     private fun XAnnotated.getSuppressValues(): Array<String>? {
         return this.toAnnotationBox(SuppressWarnings::class)?.value?.value
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt
index 1f3260d..66b4de0 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt
@@ -23,7 +23,6 @@
 import androidx.room.compiler.processing.util.kspResolver
 import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -43,7 +42,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(source)
         ) { invocation ->
             val type = invocation.processingEnv
@@ -63,7 +62,7 @@
 
     @Test
     fun synthetic() {
-        runProcessorTestIncludingKsp {
+        runProcessorTest {
             val objArray = it.processingEnv.getArrayType(
                 TypeName.OBJECT
             )
@@ -89,7 +88,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(source)
         ) { invocation ->
             val element = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -140,7 +139,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(listOf(src)) {
+        runProcessorTest(listOf(src)) {
             val subject = it.processingEnv.requireTypeElement("Subject")
             val types = subject.getAllFieldsIncludingPrivateSupers().map {
                 assertWithMessage(it.name).that(it.type.isArray()).isTrue()
@@ -170,8 +169,7 @@
     @Test
     fun createArray() {
         runKspTest(
-            sources = emptyList(),
-            succeed = true
+            sources = emptyList()
         ) { invocation ->
             val intType = invocation.processingEnv.requireType("kotlin.Int")
             invocation.processingEnv.getArrayType(intType).let {
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index d1e5326..84623ee 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -22,6 +22,7 @@
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.getParameter
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
 import androidx.room.compiler.processing.util.runProcessorTest
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.ClassName
@@ -172,7 +173,12 @@
             }
             validateElement(
                 element = it.processingEnv.requireTypeElement("foo.bar.Base"),
-                tTypeName = TypeVariableName.get("T"),
+                tTypeName = if (it.isKsp) {
+                    // when inheritance resolution happens, KSP resolves them to object
+                    TypeName.OBJECT
+                } else {
+                    TypeVariableName.get("T")
+                },
                 rTypeName = TypeVariableName.get("R")
             )
             validateElement(
@@ -344,7 +350,8 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        // enable once https://github.com/google/ksp/issues/167 is fixed
+        runProcessorTestWithoutKsp(
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -365,9 +372,14 @@
 
     @Test
     fun toStringMatchesUnderlyingElement() {
-        runProcessorTest {
-            it.processingEnv.findTypeElement("java.util.List").let { list ->
-                assertThat(list.toString()).isEqualTo("java.util.List")
+        runProcessorTest { invocation ->
+            invocation.processingEnv.findTypeElement("java.util.List").let { list ->
+                val expected = if (invocation.isKsp) {
+                    "MutableList"
+                } else {
+                    "java.util.List"
+                }
+                assertThat(list.toString()).isEqualTo(expected)
             }
         }
     }
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index 5caee8c..d4c82e9 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -23,7 +23,7 @@
 import androidx.room.compiler.processing.util.getDeclaredMethod
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.getParameter
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
@@ -38,7 +38,7 @@
 class XExecutableElementTest {
     @Test
     fun basic() {
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(
                 Source.java(
                     "foo.bar.Baz",
@@ -89,7 +89,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(subject)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -98,6 +98,26 @@
     }
 
     @Test
+    fun isVarArgs_kotlin() {
+        val subject = Source.kotlin(
+            "Subject.kt",
+            """
+            interface Subject {
+                fun method(vararg inputs: String)
+                suspend fun suspendMethod(vararg inputs: String);
+            }
+            """.trimIndent()
+        )
+        runProcessorTest(
+            sources = listOf(subject)
+        ) {
+            val element = it.processingEnv.requireTypeElement("Subject")
+            assertThat(element.getMethod("method").isVarArgs()).isTrue()
+            assertThat(element.getMethod("suspendMethod").isVarArgs()).isFalse()
+        }
+    }
+
+    @Test
     fun kotlinDefaultImpl() {
         val subject = Source.kotlin(
             "Baz.kt",
@@ -118,7 +138,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(subject)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -162,7 +182,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(src)
         ) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("Subject")
@@ -249,7 +269,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val klass = invocation.processingEnv.requireTypeElement("MyDataClass")
             val methodNames = klass.getAllMethods().map {
                 it.name
@@ -303,7 +323,7 @@
             class NullableSubject: Base<String?>()
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(source)) { invocation ->
+        runProcessorTest(sources = listOf(source)) { invocation ->
             val base = invocation.processingEnv.requireTypeElement("Base")
             val subject = invocation.processingEnv.requireType("Subject")
                 .asDeclaredType()
@@ -366,7 +386,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src, javaSrc)) { invocation ->
+        runProcessorTest(sources = listOf(src, javaSrc)) { invocation ->
             val base = invocation.processingEnv.requireTypeElement("MyInterface")
             val impl = invocation.processingEnv.requireTypeElement("MyImpl")
             val javaImpl = invocation.processingEnv.requireTypeElement("JavaImpl")
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
index b2a03e5..38b007d 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
@@ -20,7 +20,7 @@
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.UNIT_CLASS_NAME
 import androidx.room.compiler.processing.util.getMethod
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.ParameterizedTypeName
@@ -43,7 +43,7 @@
             abstract class Subject : MyInterface<String>
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(src)
         ) { invocation ->
             val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
@@ -112,7 +112,7 @@
             abstract class NullableSubject: MyInterface<String?>
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
 
             // helper method to get executable types both from sub class and also as direct child of
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
index 46b1037..b808f2c 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
@@ -23,8 +23,10 @@
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.getParameter
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
 import androidx.room.compiler.processing.util.runProcessorTest
 import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.TypeName
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -66,7 +68,8 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        // TODO run with KSP once https://github.com/google/ksp/issues/167 is fixed
+        runProcessorTestWithoutKsp(
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -251,4 +254,141 @@
             }
         }
     }
+
+    @Test
+    fun changeNullability_primitives() {
+        runProcessorTest { invocation ->
+            PRIMITIVE_TYPES.forEach { primitiveTypeName ->
+                val primitive = invocation.processingEnv.requireType(primitiveTypeName)
+                assertThat(primitive.nullability).isEqualTo(NONNULL)
+                val nullable = primitive.makeNullable()
+                assertThat(nullable.nullability).isEqualTo(NULLABLE)
+                assertThat(nullable.typeName).isEqualTo(primitiveTypeName.box())
+
+                // When a boxed primitive is marked as non-null, it should stay as boxed primitive
+                // Even though this might be counter-intutive (because making it nullable will box
+                // it) it is more consistent as it is completely valid to annotate a boxed primitive
+                // with non-null while you cannot annoteted a primitive with nullable as it is not
+                // a valid state.
+                val boxedPrimitive = invocation.processingEnv.requireType(primitiveTypeName.box())
+                val nonNull = boxedPrimitive.makeNonNullable()
+                assertThat(nonNull.nullability).isEqualTo(NONNULL)
+                assertThat(nonNull.typeName).isEqualTo(primitiveTypeName.box())
+            }
+        }
+    }
+
+    @Test
+    fun changeNullability_typeArguments() {
+        // we need to make sure we don't convert type arguments into primitives!!
+        val kotlinSrc = Source.kotlin(
+            "KotlinClas.kt",
+            """
+                class KotlinClass(val subject: List<Int?>)
+            """.trimIndent()
+        )
+        val javaSrc = Source.java(
+            "JavaClass",
+            """
+                class JavaClass {
+                    java.util.List<Integer> subject;
+                }
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(javaSrc, kotlinSrc)) { invocation ->
+            listOf("KotlinClass", "JavaClass").forEach {
+                val subject = invocation.processingEnv.requireTypeElement(it)
+                    .getField("subject").type
+                check(subject.isDeclared())
+                val typeArg = subject.typeArguments.first()
+                assertThat(typeArg.typeName).isEqualTo(TypeName.INT.box())
+                typeArg.makeNonNullable().let {
+                    assertThat(it.typeName).isEqualTo(TypeName.INT.box())
+                    assertThat(it.nullability).isEqualTo(XNullability.NONNULL)
+                }
+                typeArg.makeNonNullable().makeNullable().let {
+                    assertThat(it.typeName).isEqualTo(TypeName.INT.box())
+                    assertThat(it.nullability).isEqualTo(NULLABLE)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun changeNullability_declared() {
+        runProcessorTest { invocation ->
+            val subject = invocation.processingEnv.requireType("java.util.List")
+            subject.makeNullable().let {
+                assertThat(it.nullability).isEqualTo(NULLABLE)
+                assertThat(it.isDeclared()).isTrue()
+            }
+            subject.makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isDeclared()).isTrue()
+            }
+            // ksp defaults to non-null so we do double conversion here to ensure it flips
+            // nullability
+            subject.makeNullable().makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isDeclared()).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun changeNullability_arrayTypes() {
+        runProcessorTest { invocation ->
+            val subject = invocation.processingEnv.getArrayType(
+                invocation.processingEnv.requireType("java.util.List")
+            )
+            subject.makeNullable().let {
+                assertThat(it.nullability).isEqualTo(NULLABLE)
+                assertThat(it.isArray()).isTrue()
+            }
+            subject.makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isArray()).isTrue()
+            }
+            // ksp defaults to non-null so we do double conversion here to ensure it flips
+            // nullability
+            subject.makeNullable().makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isArray()).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun makeNullable_void() {
+        val src = Source.java(
+            "Foo.java",
+            """
+            class Foo {
+                void subject() {}
+            }
+            """.trimIndent()
+        )
+        runProcessorTest(sources = listOf(src)) { invocation ->
+            val voidType = invocation.processingEnv.requireTypeElement("Foo")
+                .getMethod("subject").returnType
+            assertThat(voidType.typeName).isEqualTo(TypeName.VOID)
+            voidType.makeNullable().let {
+                assertThat(it.nullability).isEqualTo(NULLABLE)
+                assertThat(it.typeName).isEqualTo(TypeName.VOID.box())
+            }
+        }
+    }
+
+    companion object {
+        val PRIMITIVE_TYPES = listOf(
+            TypeName.BOOLEAN,
+            TypeName.BYTE,
+            TypeName.SHORT,
+            TypeName.INT,
+            TypeName.LONG,
+            TypeName.CHAR,
+            TypeName.FLOAT,
+            TypeName.DOUBLE,
+        )
+    }
 }
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt
index 4ae8c01..2f92301 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt
@@ -17,8 +17,7 @@
 package androidx.room.compiler.processing
 
 import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.runProcessorTestForFailedCompilation
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.JavaFile
@@ -34,7 +33,7 @@
 class XProcessingEnvTest {
     @Test
     fun getElement() {
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(
                 Source.java(
                     "foo.bar.Baz",
@@ -98,7 +97,7 @@
 
     @Test
     fun basic() {
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(
                 Source.java(
                     "foo.bar.Baz",
@@ -138,7 +137,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(source)
         ) { invocation ->
             PRIMITIVE_TYPES.flatMap {
@@ -163,7 +162,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) {
+        runProcessorTest(sources = listOf(src)) {
             it.processingEnv.requireTypeElement("foo.bar.Outer.Inner").let {
                 val className = it.className
                 assertThat(className.packageName()).isEqualTo("foo.bar")
@@ -175,7 +174,7 @@
 
     @Test
     fun findGeneratedAnnotation() {
-        runProcessorTestIncludingKsp { invocation ->
+        runProcessorTest { invocation ->
             val generatedAnnotation = invocation.processingEnv.findGeneratedAnnotation()
             assertThat(generatedAnnotation?.name).isEqualTo("Generated")
         }
@@ -200,7 +199,7 @@
             """.trimIndent()
         )
         listOf(javaSrc, kotlinSrc).forEach { src ->
-            runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+            runProcessorTest(sources = listOf(src)) { invocation ->
                 val className = ClassName.get("foo.bar", "ToBeGenerated")
                 if (invocation.processingEnv.findTypeElement(className) == null) {
                     // generate only if it doesn't exist to handle multi-round
@@ -223,14 +222,18 @@
             class Foo {}
             """.trimIndent()
         )
-        // TODO include KSP when https://github.com/google/ksp/issues/122 is fixed.
-        runProcessorTestForFailedCompilation(
+        runProcessorTest(
             sources = listOf(src)
         ) {
             it.processingEnv.messager.printMessage(
                 Diagnostic.Kind.ERROR,
                 "intentional failure"
             )
+            it.assertCompilationResult {
+                compilationDidFail()
+                    .and()
+                    .hasError("intentional failure")
+            }
         }
     }
 
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 2cd2588..4aa8ac9 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -24,9 +24,8 @@
 import androidx.room.compiler.processing.util.javaElementUtils
 import androidx.room.compiler.processing.util.kspResolver
 import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestForFailedCompilation
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
@@ -53,7 +52,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(parent)
         ) {
             val type = it.processingEnv.requireType("foo.bar.Parent") as XDeclaredType
@@ -110,8 +109,9 @@
                 }
             """.trimIndent()
         )
-        // TODO run with KSP as well once https://github.com/google/ksp/issues/107 is resolved
-        runProcessorTestForFailedCompilation(
+
+        // enable KSP once https://github.com/google/ksp/issues/107 is fixed.
+        runProcessorTestWithoutKsp(
             sources = listOf(missingTypeRef)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -127,6 +127,9 @@
                     ClassName.get("", "NotExistingType")
                 )
             }
+            it.assertCompilationResult {
+                compilationDidFail()
+            }
         }
     }
 
@@ -141,7 +144,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(subject)
         ) {
             val type = it.processingEnv.requireType("foo.bar.Baz")
@@ -174,7 +177,7 @@
 
     @Test
     fun isCollection_kotlin() {
-        runKspTest(sources = emptyList(), succeed = true) { invocation ->
+        runKspTest(sources = emptyList()) { invocation ->
             val subjects = listOf("Map" to false, "List" to true, "Set" to true)
             subjects.forEach { (subject, expected) ->
                 invocation.processingEnv.requireType("kotlin.collections.$subject").let { type ->
@@ -187,7 +190,7 @@
 
     @Test
     fun toStringMatchesUnderlyingElement() {
-        runProcessorTestIncludingKsp {
+        runProcessorTest {
             val subject = "java.lang.String"
             val expected = if (it.isKsp) {
                 it.kspResolver.getClassDeclarationByName(subject)?.toString()
@@ -212,12 +215,14 @@
                 }
             """.trimIndent()
         )
-        // TODO run with KSP as well once https://github.com/google/ksp/issues/107 is resolved
-        runProcessorTestForFailedCompilation(
+        runProcessorTest(
             sources = listOf(missingTypeRef)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
             assertThat(element.superType?.isError()).isTrue()
+            it.assertCompilationResult {
+                compilationDidFail()
+            }
         }
     }
 
@@ -256,7 +261,7 @@
 
     @Test
     fun rawType() {
-        runProcessorTestIncludingKsp {
+        runProcessorTest {
             val subject = it.processingEnv.getDeclaredType(
                 it.processingEnv.requireTypeElement(List::class),
                 it.processingEnv.requireType(String::class)
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
index 679af1c..7251136 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.room.compiler.processing.javac.JavacProcessingEnv
 import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.compiler.processing.util.runKaptTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.AssumptionViolatedException
 import org.junit.Test
@@ -478,7 +478,7 @@
         sources: List<Source> = emptyList(),
         handler: (ProcessingEnvironment) -> Unit
     ) {
-        runProcessorTest(sources) {
+        runKaptTest(sources) {
             val processingEnv = it.processingEnv
             if (processingEnv !is JavacProcessingEnv) {
                 throw AssumptionViolatedException("This test only works for java/kapt compilation")
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
index f53ce78..5cfa2d6 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
@@ -22,8 +22,10 @@
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.className
 import androidx.room.compiler.processing.util.getField
+import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.runKspTest
 import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import org.junit.Test
@@ -46,7 +48,7 @@
             """.trimIndent()
         )
 
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val base = invocation.processingEnv.requireTypeElement("BaseClass")
             val sub = invocation.processingEnv.requireType("SubClass").asDeclaredType()
             base.getField("normalInt").let { prop ->
@@ -119,7 +121,7 @@
             abstract class NullableSubject: MyInterface<String?>()
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
             val nonNullSubject = invocation.processingEnv.requireType("NonNullSubject")
                 .asDeclaredType()
@@ -174,4 +176,53 @@
             }
         }
     }
+
+    @Test
+    fun asMemberOfStatics() {
+        val kotlinSrc = Source.kotlin(
+            "KotlinClass.kt",
+            """
+            class KotlinClass {
+                companion object {
+                    @JvmStatic
+                    var staticProp: String = ""
+                    @JvmStatic
+                    fun staticFun(x:Int) {}
+                }
+            }
+            """.trimIndent()
+        )
+        val javaSrc = Source.java(
+            "JavaClass",
+            """
+            class JavaClass {
+                void staticFun(int x) {}
+                static String staticProp;
+            }
+            """.trimIndent()
+        )
+        runKspTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
+            listOf("KotlinClass", "JavaClass").forEach {
+                val typeElement = invocation.processingEnv.requireTypeElement(it)
+                typeElement.getMethod("staticFun").let { staticFun ->
+                    val asMember = staticFun.asMemberOf(typeElement.type)
+                    assertThat(asMember.returnType.typeName).isEqualTo(TypeName.VOID)
+                    assertThat(
+                        asMember.parameterTypes.single().typeName
+                    ).isEqualTo(TypeName.INT)
+                    // different codepath, execute it as well
+                    assertThat(
+                        staticFun.parameters.single().asMemberOf(typeElement.type).typeName
+                    ).isEqualTo(TypeName.INT)
+                }
+                typeElement.getField("staticProp").let { staticProp ->
+                    assertThat(
+                        staticProp.asMemberOf(typeElement.type).typeName
+                    ).isEqualTo(
+                        ClassName.get(String::class.java)
+                    )
+                }
+            }
+        }
+    }
 }
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
index c185f47..2865715 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
@@ -26,7 +26,6 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.devtools.ksp.getDeclaredFunctions
 import com.google.devtools.ksp.getDeclaredProperties
-import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
@@ -55,18 +54,18 @@
             }
             """.trimIndent()
         )
-        runTest(subjectSrc) { resolver ->
-            val subject = resolver.requireClass("foo.bar.Baz")
-            assertThat(subject.propertyType("intField").typeName(resolver))
+        runKspTest(sources = listOf(subjectSrc)) { invocation ->
+            val subject = invocation.kspResolver.requireClass("foo.bar.Baz")
+            assertThat(subject.propertyType("intField").typeName(invocation.kspResolver))
                 .isEqualTo(TypeName.INT)
-            assertThat(subject.propertyType("listOfInts").typeName(resolver))
+            assertThat(subject.propertyType("listOfInts").typeName(invocation.kspResolver))
                 .isEqualTo(
                     ParameterizedTypeName.get(
                         List::class.className(),
                         TypeName.INT.box()
                     )
                 )
-            assertThat(subject.propertyType("mutableMapOfAny").typeName(resolver))
+            assertThat(subject.propertyType("mutableMapOfAny").typeName(invocation.kspResolver))
                 .isEqualTo(
                     ParameterizedTypeName.get(
                         Map::class.className(),
@@ -74,7 +73,7 @@
                         TypeName.OBJECT,
                     )
                 )
-            val typeName = subject.propertyType("nested").typeName(resolver)
+            val typeName = subject.propertyType("nested").typeName(invocation.kspResolver)
             check(typeName is ClassName)
             assertThat(typeName.packageName()).isEqualTo("foo.bar")
             assertThat(typeName.simpleNames()).containsExactly("Baz", "Nested")
@@ -97,22 +96,25 @@
             }
             """.trimIndent()
         )
-        runTest(subjectSrc) { resolver ->
-            val subject = resolver.requireClass("Baz")
-            assertThat(subject.propertyType("intField").typeName(resolver))
-                .isEqualTo(TypeName.INT)
-            assertThat(subject.propertyType("listOfInts").typeName(resolver))
-                .isEqualTo(
-                    ParameterizedTypeName.get(
-                        List::class.className(),
-                        TypeName.INT.box()
-                    )
+        runKspTest(sources = listOf(subjectSrc)) { invocation ->
+            val subject = invocation.kspResolver.requireClass("Baz")
+            assertThat(
+                subject.propertyType("intField").typeName(invocation.kspResolver)
+            ).isEqualTo(TypeName.INT)
+            assertThat(
+                subject.propertyType("listOfInts").typeName(invocation.kspResolver)
+            ).isEqualTo(
+                ParameterizedTypeName.get(
+                    List::class.className(),
+                    TypeName.INT.box()
                 )
-            assertThat(subject.propertyType("incompleteGeneric").typeName(resolver))
-                .isEqualTo(
-                    List::class.className()
-                )
-            assertThat(subject.propertyType("nested").typeName(resolver))
+            )
+            assertThat(
+                subject.propertyType("incompleteGeneric").typeName(invocation.kspResolver)
+            ).isEqualTo(
+                List::class.className()
+            )
+            assertThat(subject.propertyType("nested").typeName(invocation.kspResolver))
                 .isEqualTo(
                     ClassName.get("", "Baz", "Nested")
                 )
@@ -132,25 +134,31 @@
             }
             """.trimIndent()
         )
-        runTest(subjectSrc, succeed = false) { resolver ->
-            val subject = resolver.requireClass("Foo")
-            assertThat(subject.propertyType("errorField").typeName(resolver))
-                .isEqualTo(ERROR_TYPE_NAME)
-            assertThat(subject.propertyType("listOfError").typeName(resolver))
-                .isEqualTo(
-                    ParameterizedTypeName.get(
-                        List::class.className(),
-                        ERROR_TYPE_NAME
-                    )
+        runKspTest(sources = listOf(subjectSrc)) { invocation ->
+            val subject = invocation.kspResolver.requireClass("Foo")
+            assertThat(
+                subject.propertyType("errorField").typeName(invocation.kspResolver)
+            ).isEqualTo(ERROR_TYPE_NAME)
+            assertThat(
+                subject.propertyType("listOfError").typeName(invocation.kspResolver)
+            ).isEqualTo(
+                ParameterizedTypeName.get(
+                    List::class.className(),
+                    ERROR_TYPE_NAME
                 )
-            assertThat(subject.propertyType("mutableMapOfDontExist").typeName(resolver))
-                .isEqualTo(
-                    ParameterizedTypeName.get(
-                        Map::class.className(),
-                        String::class.className(),
-                        ERROR_TYPE_NAME
-                    )
+            )
+            assertThat(
+                subject.propertyType("mutableMapOfDontExist").typeName(invocation.kspResolver)
+            ).isEqualTo(
+                ParameterizedTypeName.get(
+                    Map::class.className(),
+                    String::class.className(),
+                    ERROR_TYPE_NAME
                 )
+            )
+            invocation.assertCompilationResult {
+                compilationDidFail()
+            }
         }
     }
 
@@ -181,8 +189,7 @@
         // methodName -> returnType, ...paramTypes
         val golden = mutableMapOf<String, List<TypeName>>()
         runKaptTest(
-            sources = listOf(src),
-            succeed = true
+            sources = listOf(src)
         ) { invocation ->
             val env = (invocation.processingEnv as JavacProcessingEnv)
             val subject = env.delegate.elementUtils.getTypeElement("Subject")
@@ -196,8 +203,7 @@
         }
         val kspResults = mutableMapOf<String, List<TypeName>>()
         runKspTest(
-            sources = listOf(src),
-            succeed = true
+            sources = listOf(src)
         ) { invocation ->
             val env = (invocation.processingEnv as KspProcessingEnv)
             val subject = env.resolver.requireClass("Subject")
@@ -220,21 +226,6 @@
         assertThat(kspResults).containsExactlyEntriesIn(golden)
     }
 
-    private fun runTest(
-        vararg sources: Source,
-        succeed: Boolean = true,
-        handler: (Resolver) -> Unit
-    ) {
-        runKspTest(
-            sources = sources.toList(),
-            succeed = succeed
-        ) {
-            handler(
-                (it.processingEnv as KspProcessingEnv).resolver
-            )
-        }
-    }
-
     private fun KSClassDeclaration.requireProperty(name: String) = getDeclaredProperties().first {
         it.simpleName.asString() == name
     }
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
index 1e8a5a2..f6666c4 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
@@ -25,7 +25,7 @@
 import androidx.room.compiler.processing.util.className
 import androidx.room.compiler.processing.util.compileFiles
 import androidx.room.compiler.processing.util.getField
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -152,7 +152,7 @@
             class Sub1 : Base<Int, String>()
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val sub = invocation.processingEnv.requireTypeElement("Sub1")
             val base = invocation.processingEnv.requireTypeElement("Base")
             val t = base.getField("t")
@@ -197,13 +197,13 @@
     private fun runModifierTest(vararg inputs: ModifierTestInput) {
         // we'll run the test twice. once it is in source and once it is coming from a dependency.
         val sources = inputs.map(ModifierTestInput::source)
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = sources
         ) { invocation ->
             assertModifiers(invocation, inputs)
         }
         val classpath = compileFiles(sources)
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = emptyList(),
             classpath = listOf(classpath)
         ) { invocation ->
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBoxTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBoxTest.kt
new file mode 100644
index 0000000..8c694265
--- /dev/null
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBoxTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.util.runKspTest
+import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+import org.junit.Test
+import kotlin.reflect.KClass
+
+class KspReflectiveAnnotationBoxTest {
+    enum class TestEnum {
+        VAL1,
+        VAL2
+    }
+
+    annotation class TestAnnotation(
+        val strProp: String = "abc",
+        val intProp: Int = 3,
+        val enumProp: TestEnum = TestEnum.VAL2,
+        val enumArrayProp: Array<TestEnum> = [TestEnum.VAL1, TestEnum.VAL2, TestEnum.VAL1],
+        val annProp: TestAnnotation2 = TestAnnotation2(3),
+        val annArrayProp: Array<TestAnnotation2> = [TestAnnotation2(1), TestAnnotation2(5)],
+        val typeProp: KClass<*> = Int::class,
+        val typeArrayProp: Array<KClass<*>> = [Int::class, String::class]
+    )
+
+    annotation class TestAnnotation2(
+        val intProp: Int = 0
+    )
+
+    @Test
+    @TestAnnotation // putting annotation here to read it back easily :)
+    fun simple() {
+        runKspTest(sources = emptyList()) { invocation ->
+            val box = KspReflectiveAnnotationBox(
+                env = invocation.processingEnv as KspProcessingEnv,
+                annotationClass = TestAnnotation::class.java,
+                annotation = getAnnotationOnMethod("simple")
+            )
+            assertThat(box.value.strProp).isEqualTo("abc")
+            assertThat(box.value.intProp).isEqualTo(3)
+            assertThat(box.value.enumProp).isEqualTo(TestEnum.VAL2)
+            assertThat(box.value.enumArrayProp).isEqualTo(
+                arrayOf(TestEnum.VAL1, TestEnum.VAL2, TestEnum.VAL1)
+            )
+            box.getAsAnnotationBox<TestAnnotation2>("annProp").let {
+                assertThat(it.value.intProp).isEqualTo(3)
+            }
+            box.getAsAnnotationBoxArray<TestAnnotation2>("annArrayProp").let {
+                assertThat(
+                    it.map { it.value.intProp }
+                ).containsExactly(1, 5)
+            }
+            box.getAsType("typeProp")?.let {
+                assertThat(it is KspType).isTrue()
+                assertThat(it.typeName).isEqualTo(TypeName.INT)
+            }
+            box.getAsTypeList("typeArrayProp").let {
+                assertThat(it.all { it is KspType }).isTrue()
+                assertThat(it.map { it.typeName }).containsExactly(
+                    TypeName.INT, ClassName.get(String::class.java)
+                )
+            }
+        }
+    }
+
+    private inline fun <reified T : Annotation> getAnnotationOnMethod(methodName: String): T {
+        return KspReflectiveAnnotationBoxTest::class.java.getMethod(methodName).annotations
+            .first {
+                it is TestAnnotation
+            } as T
+    }
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
index 10c8f93..804df7b 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
@@ -23,7 +23,7 @@
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.runKspTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
 import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.ClassName
@@ -52,8 +52,7 @@
             """.trimIndent()
         )
         runKspTest(
-            sources = listOf(src1, src2),
-            succeed = true
+            sources = listOf(src1, src2)
         ) { invocation ->
             invocation.processingEnv.requireTypeElement("TopLevel").let {
                 assertThat(it.packageName).isEqualTo("")
@@ -93,7 +92,7 @@
             interface MyInterface {}
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
                 assertThat(it.superType).isEqualTo(
                     invocation.processingEnv.requireType("foo.bar.AbstractClass")
@@ -134,7 +133,7 @@
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("foo.bar.Outer").let {
                 assertThat(it.className).isEqualTo(ClassName.get("foo.bar", "Outer"))
                 assertThat(it.enclosingTypeElement).isNull()
@@ -163,7 +162,7 @@
             private class PrivateClass
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             fun getModifiers(element: XTypeElement): Set<String> {
                 val result = mutableSetOf<String>()
                 if (element.isAbstract()) result.add("abstract")
@@ -205,7 +204,7 @@
             interface MyInterface
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("MyClass").let {
                 assertThat(it.kindName()).isEqualTo("class")
             }
@@ -228,7 +227,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
             assertThat(baseClass.getAllFieldNames()).containsExactly("genericProp")
             val subClass = invocation.processingEnv.requireTypeElement("SubClass")
@@ -269,7 +268,7 @@
             ) : BaseClass(value)
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
             assertThat(baseClass.getAllFieldNames()).containsExactly("value")
             val subClass = invocation.processingEnv.requireTypeElement("SubClass")
@@ -317,7 +316,7 @@
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val base = invocation.processingEnv.requireTypeElement("Base")
             assertThat(base.getDeclaredMethods().names()).containsExactly(
                 "baseFun", "suspendFun", "privateBaseFun", "staticBaseFun"
@@ -371,7 +370,7 @@
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val klass = invocation.processingEnv.requireTypeElement("SubClass")
             assertThat(klass.getAllMethods().names()).containsExactly(
                 "baseMethod", "overriddenMethod", "baseCompanionMethod",
@@ -395,7 +394,7 @@
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
                 assertThat(base.getDeclaredMethods().names()).containsExactly(
                     "getX"
@@ -438,8 +437,11 @@
             class SubClass : CompanionSubject()
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("CompanionSubject")
+            assertThat(subject.getAllFieldNames()).containsExactly(
+                "mutableStatic", "immutableStatic"
+            )
             assertThat(subject.getDeclaredMethods().names()).containsExactly(
                 "getMutableStatic", "setMutableStatic", "getImmutableStatic"
             )
@@ -468,7 +470,7 @@
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
                 assertThat(base.getDeclaredMethods().names()).containsExactly(
                     "getX"
@@ -517,7 +519,7 @@
             abstract class AbstractExplicit(x:Int)
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val subjects = listOf(
                 "MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
                 "BaseWithSecondary", "Sub", "SubWith3Constructors",
@@ -573,7 +575,7 @@
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("MyInterface")
             assertThat(subject.getMethod("notJvmDefault").isJavaDefault()).isFalse()
             assertThat(subject.getMethod("jvmDefault").isJavaDefault()).isTrue()
@@ -620,7 +622,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val subjects = listOf(
                 "MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
                 "BaseWithSecondary", "Sub", "SubWith3Constructors",
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
index db0fc7dc..7643b04 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
@@ -50,7 +50,7 @@
             interface MyInterface {}
             """.trimIndent()
         )
-        runKspTest(listOf(src), succeed = true) {
+        runKspTest(listOf(src)) {
             val subject = it.processingEnv.requireType("foo.bar.Baz")
             assertThat(subject.typeName).isEqualTo(
                 ClassName.get("foo.bar", "Baz")
@@ -89,8 +89,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = false
+            listOf(src)
         ) { invocation ->
             invocation.requireDeclaredPropertyType("errorType").let { type ->
                 assertThat(type.isError()).isTrue()
@@ -107,6 +106,9 @@
                     assertThat(typeArg.typeName).isEqualTo(ERROR_TYPE_NAME)
                 }
             }
+            invocation.assertCompilationResult {
+                compilationDidFail()
+            }
         }
     }
 
@@ -121,8 +123,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = true
+            listOf(src)
         ) { invocation ->
             invocation.requireDeclaredPropertyType("listOfNullableStrings").let { type ->
                 assertThat(type.nullability).isEqualTo(NONNULL)
@@ -173,8 +174,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = true
+            listOf(src)
         ) { invocation ->
             val nullableStringList = invocation
                 .requireDeclaredPropertyType("listOfNullableStrings")
@@ -220,8 +220,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = true
+            listOf(src)
         ) { invocation ->
             invocation.requirePropertyType("simple").let {
                 assertThat(it.rawType.typeName).isEqualTo(TypeName.INT)
@@ -257,7 +256,7 @@
             }
             """.trimIndent()
         )
-        runKspTest(sources = listOf(src), succeed = true) { invocation ->
+        runKspTest(sources = listOf(src)) { invocation ->
             val resolver = (invocation.processingEnv as KspProcessingEnv).resolver
             val voidMethod = resolver.getClassDeclarationByName("foo.bar.Baz")!!
                 .getDeclaredFunctions()
@@ -289,8 +288,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = false
+            listOf(src)
         ) { invocation ->
             fun mapProp(name: String) = invocation.requirePropertyType(name).let {
                 listOf(
@@ -313,6 +311,9 @@
             assertThat(mapProp("nullableByteProp")).containsExactly("isByte")
             assertThat(mapProp("errorProp")).containsExactly("isError")
             assertThat(mapProp("nullableErrorProp")).containsExactly("isError")
+            invocation.assertCompilationResult {
+                compilationDidFail()
+            }
         }
     }
 
@@ -334,8 +335,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = false
+            listOf(src)
         ) { invocation ->
             fun getDefaultValue(name: String) = invocation.requirePropertyType(name).defaultValue()
             // javac types do not check nullability but checking it is more correct
@@ -351,6 +351,9 @@
             assertThat(getDefaultValue("errorProp")).isEqualTo("null")
             assertThat(getDefaultValue("nullableErrorProp")).isEqualTo("null")
             assertThat(getDefaultValue("stringProp")).isEqualTo("null")
+            invocation.assertCompilationResult {
+                compilationDidFail()
+            }
         }
     }
 
@@ -366,8 +369,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = true
+            listOf(src)
         ) { invocation ->
             assertThat(
                 invocation.requirePropertyType("stringProp").isTypeOf(
@@ -418,8 +420,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = true
+            listOf(src)
         ) { invocation ->
             fun check(prop1: String, prop2: String): Boolean {
                 return invocation.requirePropertyType(prop1).isSameType(
@@ -450,8 +451,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = true
+            listOf(src)
         ) { invocation ->
             val env = (invocation.processingEnv as KspProcessingEnv)
             val classNames = listOf("Bar", "Bar_NullableFoo")
@@ -491,8 +491,7 @@
             """.trimIndent()
         )
         runKspTest(
-            listOf(src),
-            succeed = true
+            listOf(src)
         ) { invocation ->
             val env = (invocation.processingEnv as KspProcessingEnv)
             val method = env.resolver
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaAnnotationWithDefaults.java b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaAnnotationWithDefaults.java
new file mode 100644
index 0000000..26361dc
--- /dev/null
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaAnnotationWithDefaults.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.testcode;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+
+public @interface JavaAnnotationWithDefaults {
+    String stringVal() default "foo";
+    String[] stringArrayVal() default {"x", "y"};
+    Class<?> typeVal() default HashMap.class;
+    Class[] typeArrayVal() default {LinkedHashMap.class};
+    int intVal() default 3;
+    JavaEnum enumVal() default JavaEnum.DEFAULT;
+    JavaEnum[] enumArrayVal() default {JavaEnum.VAL1, JavaEnum.VAL2};
+    OtherAnnotation otherAnnotationVal() default @OtherAnnotation("def");
+    OtherAnnotation[] otherAnnotationArrayVal() default {@OtherAnnotation("v1")};
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaEnum.java
similarity index 79%
rename from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
rename to room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaEnum.java
index f9cb2fe..c0a037b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaEnum.java
@@ -14,7 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+package androidx.room.compiler.processing.testcode;
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+public enum JavaEnum {
+    VAL1,
+    VAL2,
+    DEFAULT
+}
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
index 0bc2a7d..1e24f10 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
@@ -20,9 +20,13 @@
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import com.google.devtools.ksp.processing.Resolver
 import javax.lang.model.util.Elements
+import javax.lang.model.util.Types
 
 val XTestInvocation.kspResolver: Resolver
     get() = (processingEnv as KspProcessingEnv).resolver
 
 val XTestInvocation.javaElementUtils: Elements
-    get() = (processingEnv as JavacProcessingEnv).elementUtils
\ No newline at end of file
+    get() = (processingEnv as JavacProcessingEnv).elementUtils
+
+val XTestInvocation.javaTypeUtils: Types
+    get() = (processingEnv as JavacProcessingEnv).typeUtils
\ No newline at end of file
diff --git a/room/compiler/build.gradle b/room/compiler/build.gradle
index fc529d1..6cca78d 100644
--- a/room/compiler/build.gradle
+++ b/room/compiler/build.gradle
@@ -114,6 +114,7 @@
     implementation(INTELLIJ_ANNOTATIONS)
     testImplementation(GOOGLE_COMPILE_TESTING)
     testImplementation projectOrArtifact(":paging:paging-common")
+    testImplementation(project(":room:room-compiler-processing-testing"))
     testImplementation(JUNIT)
     testImplementation(JSR250)
     testImplementation(MOCKITO_CORE)
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
index 3babfd8..ba1a146 100644
--- a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -131,6 +131,9 @@
     val STRING = ClassName.get("java.lang", "String")
     val INTEGER = ClassName.get("java.lang", "Integer")
     val OPTIONAL = ClassName.get("java.util", "Optional")
+    val ILLEGAL_ARG_EXCEPTION = ClassName.get(
+        "java.lang", "IllegalArgumentException"
+    )
 }
 
 object GuavaBaseTypeNames {
diff --git a/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt b/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
index d18778e..0ce7b73 100644
--- a/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
@@ -75,9 +75,9 @@
         messager.printMessage(WARNING, msg.safeFormat(args), defaultElement)
     }
 
-    class CollectingMessager : XMessager {
+    class CollectingMessager : XMessager() {
         private val messages = mutableMapOf<Diagnostic.Kind, MutableList<Pair<String, XElement?>>>()
-        override fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+        override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
             messages.getOrPut(
                 kind,
                 {
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
index 7a3664c..2e53e25 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
@@ -25,8 +25,8 @@
  */
 object BoxedBooleanToBoxedIntConverter {
     fun create(processingEnvironment: XProcessingEnv): List<TypeConverter> {
-        val tBoolean = processingEnvironment.requireType("java.lang.Boolean")
-        val tInt = processingEnvironment.requireType("java.lang.Integer")
+        val tBoolean = processingEnvironment.requireType("java.lang.Boolean").makeNullable()
+        val tInt = processingEnvironment.requireType("java.lang.Integer").makeNullable()
         return listOf(
             object : TypeConverter(tBoolean, tInt) {
                 override fun convert(
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
index 6fc5cc5..e47d917 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
@@ -34,7 +34,7 @@
 
             return primitiveAdapters.map {
                 BoxedPrimitiveColumnTypeAdapter(
-                    it.out.boxed(),
+                    it.out.boxed().makeNullable(),
                     it
                 )
             }
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
index 1194b1d..090dfc8 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,29 +16,38 @@
 
 package androidx.room.solver.types
 
+import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XType
+import androidx.room.ext.CommonTypeNames.ILLEGAL_ARG_EXCEPTION
 import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.S
 import androidx.room.ext.T
 import androidx.room.parser.SQLTypeAffinity.TEXT
 import androidx.room.solver.CodeGenScope
+import androidx.room.writer.ClassWriter
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import java.util.Locale
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.Modifier
 
 /**
  * Uses enum string representation.
  */
 class EnumColumnTypeAdapter(out: XType) :
     ColumnTypeAdapter(out, TEXT) {
-    private val enumTypeName = out.typeName
     override fun readFromCursor(
         outVarName: String,
         cursorVarName: String,
         indexVarName: String,
         scope: CodeGenScope
     ) {
+        val stringToEnumMethod = stringToEnumMethod(scope)
         scope.builder()
             .addStatement(
-                "$L = $T.valueOf($L.getString($L))", outVarName, enumTypeName,
-                cursorVarName,
-                indexVarName
+                "$L = $N($L.getString($L))",
+                outVarName, stringToEnumMethod, cursorVarName, indexVarName
             )
     }
 
@@ -48,12 +57,102 @@
         valueVarName: String,
         scope: CodeGenScope
     ) {
+        val enumToStringMethod = enumToStringMethod(scope)
         scope.builder().apply {
             beginControlFlow("if ($L == null)", valueVarName)
                 .addStatement("$L.bindNull($L)", stmtName, indexVarName)
             nextControlFlow("else")
-                .addStatement("$L.bindString($L, $L.name())", stmtName, indexVarName, valueVarName)
+                .addStatement(
+                    "$L.bindString($L, $N($L))",
+                    stmtName, indexVarName, enumToStringMethod, valueVarName
+                )
             endControlFlow()
         }
     }
-}
\ No newline at end of file
+
+    private fun enumToStringMethod(scope: CodeGenScope): MethodSpec {
+        return scope.writer.getOrCreateMethod(object :
+                ClassWriter.SharedMethodSpec(out.asTypeElement().name + "_enumToString") {
+                override fun getUniqueKey(): String {
+                    return "enumToString_" + out.typeName.toString()
+                }
+
+                override fun prepare(
+                    methodName: String,
+                    writer: ClassWriter,
+                    builder: MethodSpec.Builder
+                ) {
+                    builder.apply {
+                        addModifiers(Modifier.PRIVATE)
+                        returns(String::class.java)
+                        val param = ParameterSpec.builder(
+                            out.typeName, "_value", Modifier.FINAL
+                        ).build()
+                        addParameter(param)
+                        beginControlFlow("if ($N == null)", param)
+                        addStatement("return null")
+                        nextControlFlow("switch ($N)", param)
+                        getEnumConstantElements().forEach { enumConstant ->
+                            addStatement("case $L: return $S", enumConstant.name, enumConstant.name)
+                        }
+                        addStatement(
+                            "default: throw new $T($S)",
+                            ILLEGAL_ARG_EXCEPTION,
+                            "Can't convert ${param.name} to string, unknown enum value."
+                        )
+                        endControlFlow()
+                    }
+                }
+            })
+    }
+
+    private fun stringToEnumMethod(scope: CodeGenScope): MethodSpec {
+        return scope.writer.getOrCreateMethod(object :
+                ClassWriter.SharedMethodSpec(out.asTypeElement().name + "_stringToEnum") {
+                override fun getUniqueKey(): String {
+                    return out.typeName.toString()
+                }
+
+                override fun prepare(
+                    methodName: String,
+                    writer: ClassWriter,
+                    builder: MethodSpec.Builder
+                ) {
+                    builder.apply {
+                        addModifiers(Modifier.PRIVATE)
+                        returns(out.typeName)
+                        val param = ParameterSpec.builder(
+                            String::class.java, "_value", Modifier.FINAL
+                        ).build()
+                        addParameter(param)
+                        beginControlFlow("if ($N == null)", param)
+                        addStatement("return null")
+                        nextControlFlow("switch ($N)", param)
+                        getEnumConstantElements().forEach {
+                            enumConstant ->
+                            addStatement(
+                                "case $S: return $T.$L",
+                                enumConstant.name, out.typeName, enumConstant.name
+                            )
+                        }
+                        addStatement(
+                            "default: throw new $T($S)",
+                            ILLEGAL_ARG_EXCEPTION,
+                            "Can't convert ${param.name} to enum, unknown value."
+                        )
+                        endControlFlow()
+                    }
+                }
+            })
+    }
+
+    private fun getEnumConstantElements(): List<XFieldElement> {
+        // TODO: Switch below logic to use`getDeclaredFields` when the
+        //  functionality is available in the XTypeElement API
+        val typeElementFields = out.asTypeElement().getAllFieldsIncludingPrivateSupers()
+        return typeElementFields.filter {
+            // TODO: (b/173236324) Add kind to the X abstraction API to avoid using kindName()
+            ElementKind.ENUM_CONSTANT.toString().toLowerCase(Locale.US) == it.kindName()
+        }
+    }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt b/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt
index 8041f42..5655d9b 100644
--- a/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt
@@ -23,8 +23,11 @@
  * [androidx.room.RoomProcessor.methodParametersVisibleInClassFiles] check only. If you want to use
  * this class, consider expanding the implementation or use a different library.
  */
-data class SimpleJavaVersion(val major: Int, val minor: Int, val update: Int? = null) :
-    Comparable<SimpleJavaVersion> {
+data class SimpleJavaVersion(
+    val major: Int,
+    val minor: Int,
+    val update: Int? = null
+) : Comparable<SimpleJavaVersion> {
 
     override fun compareTo(other: SimpleJavaVersion): Int {
         return compareValuesBy(
@@ -59,13 +62,23 @@
 
             val parts = version.split('.')
 
-            // There are valid JDK version strings with more than 3 parts when split by dots.
-            // For example: "11.0.6+10-post-Ubuntu-1ubuntu118.04.1".
-            if (parts.size < 3) {
-                return null
+            // There are valid JDK version strings with no parts split by dots.
+            // For example: "15+36".
+            if (parts.size == 1) {
+                return try {
+                    val major = parts[0].substringBeforeNonDigitChar()
+                    SimpleJavaVersion(major.toInt(), 0)
+                } catch (e: NumberFormatException) {
+                    null
+                }
             }
 
             if (parts[0] == "1") {
+                // All 3 parts are needed when JDK versions strings where major version is 1.
+                // For example: "1.8.0_202-release-1483-b39-5396753"
+                if (parts.size < 3) {
+                    return null
+                }
                 val major = parts[1]
                 val minorAndUpdate = parts[2].substringBefore('-').split('_')
                 if (minorAndUpdate.size != 2) {
@@ -82,13 +95,23 @@
                 }
             } else {
                 return try {
-                    SimpleJavaVersion(parts[0].toInt(), parts[1].toInt())
+                    val minor = parts[1].substringBeforeNonDigitChar()
+                    SimpleJavaVersion(parts[0].toInt(), minor.toInt())
                 } catch (e: NumberFormatException) {
                     null
                 }
             }
         }
 
+        private fun String.substringBeforeNonDigitChar(): String {
+            val nonDigitIndex = indexOfFirst { !it.isDigit() }
+            return if (nonDigitIndex == -1) {
+                this
+            } else {
+                substring(0, nonDigitIndex)
+            }
+        }
+
         /**
          * Parses the Java version from the given string (e.g.,
          * "1.8.0_202-release-1483-b39-5396753"), throwing [IllegalArgumentException] if it
diff --git a/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt b/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt
index b87099d..71a4ce0 100644
--- a/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt
@@ -16,20 +16,22 @@
 
 package androidx.room.log
 
+import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XMessager
 import androidx.room.vo.Warning
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
+import javax.tools.Diagnostic
 
 @RunWith(JUnit4::class)
 class RLogTest {
-
-    val messager = mock(XMessager::class.java)
-
     @Test
     fun testSafeFormat() {
+        val messager = object : XMessager() {
+            override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+            }
+        }
         val logger = RLog(messager, emptySet(), null)
 
         // UnknownFormatConversionException
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index f7ad69b..ab48fa7a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -19,9 +19,11 @@
 import COMMON
 import androidx.paging.DataSource
 import androidx.paging.PagingSource
-import androidx.room.Entity
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.asDeclaredType
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.L
 import androidx.room.ext.LifecyclesTypeNames
@@ -46,12 +48,7 @@
 import androidx.room.solver.types.CompositeAdapter
 import androidx.room.solver.types.EnumColumnTypeAdapter
 import androidx.room.solver.types.TypeConverter
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
+import androidx.room.testing.context
 import com.squareup.javapoet.TypeName
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.CoreMatchers.instanceOf
@@ -61,8 +58,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import simpleRun
 import testCodeGenScope
+import toSources
 
 @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
 @RunWith(JUnit4::class)
@@ -73,21 +70,24 @@
 
     @Test
     fun testDirect() {
-        singleRun { invocation ->
+        runProcessorTest { invocation ->
             val store = TypeAdapterStore.create(Context(invocation.processingEnv))
             val primitiveType = invocation.processingEnv.requireType(TypeName.INT)
             val adapter = store.findColumnTypeAdapter(primitiveType, null)
             assertThat(adapter, notNullValue())
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testJavaLangBoolean() {
-        singleRun { invocation ->
-            val store = TypeAdapterStore.create(Context(invocation.processingEnv))
+        runProcessorTest { invocation ->
+            val store = TypeAdapterStore.create(
+                Context(invocation.processingEnv)
+            )
             val boolean = invocation
                 .processingEnv
                 .requireType("java.lang.Boolean")
+                .makeNullable()
             val adapter = store.findColumnTypeAdapter(boolean, null)
             assertThat(adapter, notNullValue())
             assertThat(adapter, instanceOf(CompositeAdapter::class.java))
@@ -100,22 +100,23 @@
                 composite.columnTypeAdapter.out.typeName,
                 `is`(TypeName.INT.box())
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testJavaLangEnumCompilesWithoutError() {
-        simpleRun(
-            JavaFileObjects.forSourceString(
-                "foo.bar.Fruit",
-                """ package foo.bar;
+        val enumSrc = Source.java(
+            "foo.bar.Fruit",
+            """ package foo.bar;
                 import androidx.room.*;
                 enum Fruit {
                     APPLE,
                     BANANA,
                     STRAWBERRY}
                 """.trimMargin()
-            )
+        )
+        runProcessorTest(
+            sources = listOf(enumSrc)
         ) { invocation ->
             val store = TypeAdapterStore.create(Context(invocation.processingEnv))
             val enum = invocation
@@ -124,12 +125,12 @@
             val adapter = store.findColumnTypeAdapter(enum, null)
             assertThat(adapter, notNullValue())
             assertThat(adapter, instanceOf(EnumColumnTypeAdapter::class.java))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testVia1TypeAdapter() {
-        singleRun { invocation ->
+        runProcessorTest { invocation ->
             val store = TypeAdapterStore.create(Context(invocation.processingEnv))
             val booleanType = invocation.processingEnv.requireType(TypeName.BOOLEAN)
             val adapter = store.findColumnTypeAdapter(booleanType, null)
@@ -160,12 +161,35 @@
                     """.trimIndent()
                 )
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testVia2TypeAdapters() {
-        singleRun { invocation ->
+        val point = Source.java(
+            "foo.bar.Point",
+            """
+            package foo.bar;
+            import androidx.room.*;
+            @Entity
+            public class Point {
+                public int x, y;
+                public Point(int x, int y) {
+                    this.x = x;
+                    this.y = y;
+                }
+                public static Point fromBoolean(boolean val) {
+                    return val ? new Point(1, 1) : new Point(0, 0);
+                }
+                public static boolean toBoolean(Point point) {
+                    return point.x > 0;
+                }
+            }
+            """
+        )
+        runProcessorTest(
+            sources = listOf(point)
+        ) { invocation ->
             val store = TypeAdapterStore.create(
                 Context(invocation.processingEnv),
                 pointTypeConverters(invocation.processingEnv)
@@ -204,17 +228,17 @@
                     """.trimIndent()
                 )
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testDate() {
-        singleRun { (processingEnv) ->
+        runProcessorTest { invocation ->
             val store = TypeAdapterStore.create(
-                Context(processingEnv),
-                dateTypeConverters(processingEnv)
+                invocation.context,
+                dateTypeConverters(invocation.processingEnv)
             )
-            val tDate = processingEnv.requireType("java.util.Date")
+            val tDate = invocation.processingEnv.requireType("java.util.Date")
             val adapter = store.findCursorValueReader(tDate, SQLTypeAffinity.INTEGER)
             assertThat(adapter, notNullValue())
             assertThat(adapter?.typeMirror(), `is`(tDate))
@@ -234,12 +258,12 @@
                     """.trimIndent()
                 )
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testIntList() {
-        singleRun { invocation ->
+        runProcessorTest { invocation ->
             val binders = createIntListToStringBinders(invocation)
             val store = TypeAdapterStore.create(
                 Context(invocation.processingEnv), binders[0],
@@ -272,12 +296,12 @@
             )
             assertThat(converter, notNullValue())
             assertThat(store.reverse(converter!!), `is`(binders[1]))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testOneWayConversion() {
-        singleRun { invocation ->
+        runProcessorTest { invocation ->
             val binders = createIntListToStringBinders(invocation)
             val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0])
             val adapter = store.findColumnTypeAdapter(binders[0].from, null)
@@ -298,7 +322,9 @@
     @Test
     fun testMissingRx2Room() {
         @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.PUBLISHER, COMMON.RX2_FLOWABLE)) { invocation ->
+        runProcessorTest(
+            sources = listOf(COMMON.PUBLISHER, COMMON.RX2_FLOWABLE).toSources()
+        ) { invocation ->
             val publisherElement = invocation.processingEnv
                 .requireTypeElement(ReactiveStreamsTypeNames.PUBLISHER)
             assertThat(publisherElement, notNullValue())
@@ -308,13 +334,18 @@
                 },
                 `is`(true)
             )
-        }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
+            invocation.assertCompilationResult {
+                hasError(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
+            }
+        }
     }
 
     @Test
     fun testMissingRx3Room() {
         @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.PUBLISHER, COMMON.RX3_FLOWABLE)) { invocation ->
+        runProcessorTest(
+            sources = listOf(COMMON.PUBLISHER, COMMON.RX3_FLOWABLE).toSources()
+        ) { invocation ->
             val publisherElement = invocation.processingEnv
                 .requireTypeElement(ReactiveStreamsTypeNames.PUBLISHER)
             assertThat(publisherElement, notNullValue())
@@ -324,7 +355,10 @@
                 },
                 `is`(true)
             )
-        }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA3_ARTIFACT)
+            invocation.assertCompilationResult {
+                hasError(ProcessorErrors.MISSING_ROOM_RXJAVA3_ARTIFACT)
+            }
+        }
     }
 
     @Test
@@ -334,8 +368,9 @@
             COMMON.RX3_FLOWABLE to COMMON.RX3_ROOM
         ).forEach { (rxTypeSrc, rxRoomSrc) ->
             @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-            simpleRun(jfos = arrayOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc)) {
-                invocation ->
+            runProcessorTest(
+                sources = listOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc).toSources()
+            ) { invocation ->
                 val publisher = invocation.processingEnv
                     .requireTypeElement(ReactiveStreamsTypeNames.PUBLISHER)
                 assertThat(publisher, notNullValue())
@@ -345,7 +380,7 @@
                     },
                     `is`(true)
                 )
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -356,8 +391,9 @@
             Triple(COMMON.RX3_FLOWABLE, COMMON.RX3_ROOM, RxJava3TypeNames.FLOWABLE)
         ).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
             @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-            simpleRun(jfos = arrayOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc)) {
-                invocation ->
+            runProcessorTest(
+                sources = listOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc).toSources()
+            ) { invocation ->
                 val flowable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
                 assertThat(
                     RxQueryResultBinderProvider.getAll(invocation.context).any {
@@ -365,7 +401,7 @@
                     },
                     `is`(true)
                 )
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -376,8 +412,9 @@
             Triple(COMMON.RX3_OBSERVABLE, COMMON.RX3_ROOM, RxJava3TypeNames.OBSERVABLE)
         ).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
             @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-            simpleRun(jfos = arrayOf(rxTypeSrc, rxRoomSrc)) {
-                invocation ->
+            runProcessorTest(
+                sources = listOf(rxTypeSrc, rxRoomSrc).toSources()
+            ) { invocation ->
                 val observable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
                 assertThat(observable, notNullValue())
                 assertThat(
@@ -386,7 +423,7 @@
                     },
                     `is`(true)
                 )
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -397,8 +434,7 @@
             Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
         ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
             @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-            simpleRun(jfos = arrayOf(rxTypeSrc)) {
-                invocation ->
+            runProcessorTest(sources = listOf(rxTypeSrc).toSources()) { invocation ->
                 val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
                 assertThat(single, notNullValue())
                 assertThat(
@@ -407,7 +443,7 @@
                     },
                     `is`(true)
                 )
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -417,9 +453,7 @@
             Triple(COMMON.RX2_MAYBE, COMMON.RX2_ROOM, RxJava2TypeNames.MAYBE),
             Triple(COMMON.RX3_MAYBE, COMMON.RX3_ROOM, RxJava3TypeNames.MAYBE)
         ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
-            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-            simpleRun(jfos = arrayOf(rxTypeSrc)) {
-                invocation ->
+            runProcessorTest(sources = listOf(rxTypeSrc).toSources()) { invocation ->
                 val maybe = invocation.processingEnv.requireTypeElement(rxTypeClassName)
                 assertThat(
                     RxCallableInsertMethodBinderProvider.getAll(invocation.context).any {
@@ -427,7 +461,7 @@
                     },
                     `is`(true)
                 )
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -437,9 +471,7 @@
             Triple(COMMON.RX2_COMPLETABLE, COMMON.RX2_ROOM, RxJava2TypeNames.COMPLETABLE),
             Triple(COMMON.RX3_COMPLETABLE, COMMON.RX3_ROOM, RxJava3TypeNames.COMPLETABLE)
         ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
-            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-            simpleRun(jfos = arrayOf(rxTypeSrc)) {
-                invocation ->
+            runProcessorTest(sources = listOf(rxTypeSrc).toSources()) { invocation ->
                 val completable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
                 assertThat(
                     RxCallableInsertMethodBinderProvider.getAll(invocation.context).any {
@@ -447,14 +479,13 @@
                     },
                     `is`(true)
                 )
-            }.compilesWithoutError()
+            }
         }
     }
 
     @Test
     fun testFindInsertListenableFuture() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.LISTENABLE_FUTURE)) {
+        runProcessorTest(sources = listOf(COMMON.LISTENABLE_FUTURE).toSources()) {
             invocation ->
             val future = invocation.processingEnv
                 .requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
@@ -464,14 +495,12 @@
                 ),
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testFindDeleteOrUpdateSingle() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.RX2_SINGLE)) {
-            invocation ->
+        runProcessorTest(sources = listOf(COMMON.RX2_SINGLE).toSources()) { invocation ->
             val single = invocation.processingEnv.requireTypeElement(RxJava2TypeNames.SINGLE)
             assertThat(single, notNullValue())
             assertThat(
@@ -480,13 +509,12 @@
                 },
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testFindDeleteOrUpdateMaybe() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.RX2_MAYBE)) {
+        runProcessorTest(sources = listOf(COMMON.RX2_MAYBE).toSources()) {
             invocation ->
             val maybe = invocation.processingEnv.requireTypeElement(RxJava2TypeNames.MAYBE)
             assertThat(maybe, notNullValue())
@@ -496,13 +524,12 @@
                 },
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testFindDeleteOrUpdateCompletable() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.RX2_COMPLETABLE)) {
+        runProcessorTest(sources = listOf(COMMON.RX2_COMPLETABLE).toSources()) {
             invocation ->
             val completable = invocation.processingEnv
                 .requireTypeElement(RxJava2TypeNames.COMPLETABLE)
@@ -513,14 +540,14 @@
                 },
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testFindDeleteOrUpdateListenableFuture() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.LISTENABLE_FUTURE)) {
-            invocation ->
+        runProcessorTest(
+            sources = listOf(COMMON.LISTENABLE_FUTURE).toSources()
+        ) { invocation ->
             val future = invocation.processingEnv
                 .requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
             assertThat(future, notNullValue())
@@ -529,14 +556,14 @@
                     .matches(future.asDeclaredType()),
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun testFindLiveData() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) {
-            invocation ->
+        runProcessorTest(
+            sources = listOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA).toSources()
+        ) { invocation ->
             val liveData = invocation.processingEnv
                 .requireTypeElement(LifecyclesTypeNames.LIVE_DATA)
             assertThat(liveData, notNullValue())
@@ -546,12 +573,12 @@
                 ),
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun findPagingSourceIntKey() {
-        simpleRun { invocation ->
+        runProcessorTest { invocation ->
             val pagingSourceElement = invocation.processingEnv
                 .requireTypeElement(PagingSource::class)
             val intType = invocation.processingEnv.requireType(Integer::class)
@@ -569,7 +596,7 @@
 
     @Test
     fun findPagingSourceStringKey() {
-        simpleRun { invocation ->
+        runProcessorTest { invocation ->
             val pagingSourceElement = invocation.processingEnv
                 .requireTypeElement(PagingSource::class)
             val stringType = invocation.processingEnv.requireType(String::class)
@@ -582,12 +609,15 @@
                     .matches(pagingSourceIntIntType.asDeclaredType()),
                 `is`(true)
             )
-        }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_PAGING_SOURCE_TYPE)
+            invocation.assertCompilationResult {
+                hasError(ProcessorErrors.PAGING_SPECIFY_PAGING_SOURCE_TYPE)
+            }
+        }
     }
 
     @Test
     fun findDataSource() {
-        simpleRun {
+        runProcessorTest {
             invocation ->
             val dataSource = invocation.processingEnv.requireTypeElement(DataSource::class)
             assertThat(dataSource, notNullValue())
@@ -597,12 +627,15 @@
                 ),
                 `is`(true)
             )
-        }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
+            invocation.assertCompilationResult {
+                hasError(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
+            }
+        }
     }
 
     @Test
     fun findPositionalDataSource() {
-        simpleRun {
+        runProcessorTest {
             invocation ->
             @Suppress("DEPRECATION")
             val dataSource = invocation.processingEnv
@@ -614,13 +647,12 @@
                 ),
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun findDataSourceFactory() {
-        @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
-        simpleRun(jfos = arrayOf(COMMON.DATA_SOURCE_FACTORY)) {
+        runProcessorTest(sources = listOf(COMMON.DATA_SOURCE_FACTORY).toSources()) {
             invocation ->
             val pagedListProvider = invocation.processingEnv
                 .requireTypeElement(PagingTypeNames.DATA_SOURCE_FACTORY)
@@ -631,14 +663,13 @@
                 ),
                 `is`(true)
             )
-        }.compilesWithoutError()
+        }
     }
 
-    private fun createIntListToStringBinders(invocation: TestInvocation): List<TypeConverter> {
+    private fun createIntListToStringBinders(invocation: XTestInvocation): List<TypeConverter> {
         val intType = invocation.processingEnv.requireType(Integer::class)
         val listElement = invocation.processingEnv.requireTypeElement(java.util.List::class)
         val listOfInts = invocation.processingEnv.getDeclaredType(listElement, intType)
-
         val intListConverter = object : TypeConverter(
             listOfInts,
             invocation.context.COMMON_TYPES.STRING
@@ -676,53 +707,6 @@
         return listOf(intListConverter, stringToIntListConverter)
     }
 
-    fun singleRun(handler: (TestInvocation) -> Unit): CompileTester {
-        return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
-            .that(
-                listOf(
-                    JavaFileObjects.forSourceString(
-                        "foo.bar.DummyClass",
-                        """
-                        package foo.bar;
-                        import androidx.room.*;
-                        @Entity
-                        public class DummyClass {}
-                        """
-                    ),
-                    JavaFileObjects.forSourceString(
-                        "foo.bar.Point",
-                        """
-                        package foo.bar;
-                        import androidx.room.*;
-                        @Entity
-                        public class Point {
-                            public int x, y;
-                            public Point(int x, int y) {
-                                this.x = x;
-                                this.y = y;
-                            }
-                            public static Point fromBoolean(boolean val) {
-                                return val ? new Point(1, 1) : new Point(0, 0);
-                            }
-                            public static boolean toBoolean(Point point) {
-                                return point.x > 0;
-                            }
-                        }
-                        """
-                    )
-                )
-            )
-            .processedWith(
-                TestProcessor.builder()
-                    .forAnnotations(Entity::class)
-                    .nextRunHandler { invocation ->
-                        handler(invocation)
-                        true
-                    }
-                    .build()
-            )
-    }
-
     fun pointTypeConverters(env: XProcessingEnv): List<TypeConverter> {
         val tPoint = env.requireType("foo.bar.Point")
         val tBoolean = env.requireType(TypeName.BOOLEAN)
@@ -759,8 +743,8 @@
     }
 
     fun dateTypeConverters(env: XProcessingEnv): List<TypeConverter> {
-        val tDate = env.requireType("java.util.Date")
-        val tLong = env.requireType("java.lang.Long")
+        val tDate = env.requireType("java.util.Date").makeNullable()
+        val tLong = env.requireType("java.lang.Long").makeNullable()
         return listOf(
             object : TypeConverter(tDate, tLong) {
                 override fun convert(
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
index 2d00be6..25c629a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
@@ -16,46 +16,37 @@
 
 package androidx.room.testing
 
-import androidx.room.Query
-import com.google.common.truth.Truth
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourceSubjectFactory
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import java.util.concurrent.atomic.AtomicBoolean
 
 @RunWith(JUnit4::class)
 class InProcessorTest {
     @Test
     fun testInProcessorTestRuns() {
-        val didRun = AtomicBoolean(false)
-        Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
-            .that(
-                JavaFileObjects.forSourceString(
-                    "foo.bar.MyClass",
-                    """
-                        package foo.bar;
-                        abstract public class MyClass {
-                        @androidx.room.Query("foo")
-                        abstract public void setFoo(String foo);
-                        }
-                        """
-                )
-            )
-            .processedWith(
-                TestProcessor.builder()
-                    .nextRunHandler { invocation ->
-                        didRun.set(true)
-                        assertThat(invocation.annotations.size, `is`(1))
-                        true
-                    }
-                    .forAnnotations(Query::class)
-                    .build()
-            )
-            .compilesWithoutError()
-        assertThat(didRun.get(), `is`(true))
+        val source = Source.java(
+            qName = "foo.bar.MyClass",
+            code = """
+                package foo.bar;
+                abstract public class MyClass {
+                @androidx.room.Query("foo")
+                abstract public void setFoo(String foo);
+                }
+            """.trimIndent()
+        )
+        var runCount = 0
+        runProcessorTest(sources = listOf(source)) {
+            assertThat(
+                it.processingEnv.findTypeElement("foo.bar.MyClass")
+            ).isNotNull()
+            runCount++
+        }
+        // run 3 times: javac, kapt, ksp
+        assertThat(
+            runCount
+        ).isEqualTo(3)
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/room/compiler/src/test/kotlin/androidx/room/testing/XTestInvocationExt.kt
similarity index 71%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to room/compiler/src/test/kotlin/androidx/room/testing/XTestInvocationExt.kt
index f9cb2fe..62e386a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/XTestInvocationExt.kt
@@ -14,7 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+package androidx.room.testing
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.processor.Context
+
+val XTestInvocation.context
+    get() = getOrPutUserData(Context::class) {
+        Context(processingEnv)
+    }
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index b186bff..febe6e2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -19,6 +19,7 @@
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.util.Source
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.LifecyclesTypeNames
@@ -314,4 +315,25 @@
     return System.getProperty("java.class.path")!!.split(pathSeparator).map { File(it) }.toSet()
 }
 
-fun String.toJFO(qName: String): JavaFileObject = JavaFileObjects.forSourceLines(qName, this)
\ No newline at end of file
+fun String.toJFO(qName: String): JavaFileObject = JavaFileObjects.forSourceLines(qName, this)
+
+/**
+ * Convenience method to convert JFO's to the Source objects in XProcessing so that we can
+ * convert room tests to the common API w/o major code refactor
+ */
+fun JavaFileObject.toSource(): Source {
+    val uri = this.toUri()
+    // parse name from uri
+    val contents = this.openReader(true).use {
+        it.readText()
+    }
+    val qName = uri.path.replace('/', '.')
+    val javaExt = ".java"
+    check(qName.endsWith(javaExt)) {
+        "expected a java source file, $qName does not seem like one"
+    }
+
+    return Source.java(qName.dropLast(javaExt.length), contents)
+}
+
+fun Collection<JavaFileObject>.toSources() = map { it.toSource() }
diff --git a/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt b/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt
index 830aeba..e930fc9 100644
--- a/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.util
 
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.fail
 import org.junit.Test
 
@@ -23,37 +24,39 @@
 
     @Test
     fun testTryParse() {
-        assert(SimpleJavaVersion.tryParse("11.0.1+13-LTS") == SimpleJavaVersion(11, 0, null))
-        assert(
-            SimpleJavaVersion.tryParse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1")
-                == SimpleJavaVersion(11, 0, null)
-        )
-        assert(
-            SimpleJavaVersion.tryParse("1.8.0_202-release-1483-b39-5396753")
-                == SimpleJavaVersion(8, 0, 202)
-        )
-        assert(
-            SimpleJavaVersion.tryParse("1.8.0_181-google-v7-238857965-238857965")
-                == SimpleJavaVersion(8, 0, 181)
-        )
-        assert(SimpleJavaVersion.tryParse("a.b.c") == null)
+        assertThat(SimpleJavaVersion.tryParse("1.8.0_181-google-v7-238857965-238857965"))
+            .isEqualTo(SimpleJavaVersion(8, 0, 181))
+        assertThat(SimpleJavaVersion.tryParse("1.8.0_202-release-1483-b39-5396753"))
+            .isEqualTo(SimpleJavaVersion(8, 0, 202))
+        assertThat(SimpleJavaVersion.tryParse("11.0.1+13-LTS"))
+            .isEqualTo(SimpleJavaVersion(11, 0, null))
+        assertThat(SimpleJavaVersion.tryParse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1"))
+            .isEqualTo(SimpleJavaVersion(11, 0, null))
+        assertThat(SimpleJavaVersion.tryParse("11.0.8+10-b944.6842174"))
+            .isEqualTo(SimpleJavaVersion(11, 0, null))
+        assertThat(SimpleJavaVersion.tryParse("14.1-ea"))
+            .isEqualTo(SimpleJavaVersion(14, 1, null))
+        assertThat(SimpleJavaVersion.tryParse("15+13"))
+            .isEqualTo(SimpleJavaVersion(15, 0, null))
+        assertThat(SimpleJavaVersion.tryParse("a.b.c")).isNull()
     }
 
     @Test
     fun testParse() {
-        assert(SimpleJavaVersion.parse("11.0.1+13-LTS") == SimpleJavaVersion(11, 0, null))
-        assert(
-            SimpleJavaVersion.parse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1")
-                == SimpleJavaVersion(11, 0, null)
-        )
-        assert(
-            SimpleJavaVersion.parse("1.8.0_202-release-1483-b39-5396753")
-                == SimpleJavaVersion(8, 0, 202)
-        )
-        assert(
-            SimpleJavaVersion.parse("1.8.0_181-google-v7-238857965-238857965")
-                == SimpleJavaVersion(8, 0, 181)
-        )
+        assertThat(SimpleJavaVersion.parse("1.8.0_181-google-v7-238857965-238857965"))
+            .isEqualTo(SimpleJavaVersion(8, 0, 181))
+        assertThat(SimpleJavaVersion.parse("1.8.0_202-release-1483-b39-5396753"))
+            .isEqualTo(SimpleJavaVersion(8, 0, 202))
+        assertThat(SimpleJavaVersion.parse("11.0.1+13-LTS"))
+            .isEqualTo(SimpleJavaVersion(11, 0, null))
+        assertThat(SimpleJavaVersion.parse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1"))
+            .isEqualTo(SimpleJavaVersion(11, 0, null))
+        assertThat(SimpleJavaVersion.parse("11.0.8+10-b944.6842174"))
+            .isEqualTo(SimpleJavaVersion(11, 0, null))
+        assertThat(SimpleJavaVersion.parse("14.1-ea"))
+            .isEqualTo(SimpleJavaVersion(14, 1, null))
+        assertThat(SimpleJavaVersion.parse("15+13"))
+            .isEqualTo(SimpleJavaVersion(15, 0, null))
         try {
             SimpleJavaVersion.parse("a.b.c")
             fail("Expected IllegalArgumentException")
@@ -64,19 +67,19 @@
 
     @Test
     fun testComparison() {
-        assert(SimpleJavaVersion(11, 1) > SimpleJavaVersion(8, 2))
-        assert(SimpleJavaVersion(8, 2) < SimpleJavaVersion(11, 1))
-        assert(SimpleJavaVersion(8, 1) == SimpleJavaVersion(8, 1))
+        assertThat(SimpleJavaVersion(11, 1)).isGreaterThan(SimpleJavaVersion(8, 2))
+        assertThat(SimpleJavaVersion(8, 2)).isLessThan(SimpleJavaVersion(11, 1))
+        assertThat(SimpleJavaVersion(8, 1)).isEqualTo(SimpleJavaVersion(8, 1))
 
-        assert(SimpleJavaVersion(8, 2, 1) > SimpleJavaVersion(8, 1, 2))
-        assert(SimpleJavaVersion(8, 1, 2) < SimpleJavaVersion(8, 2, 1))
-        assert(SimpleJavaVersion(8, 1, null) == SimpleJavaVersion(8, 1, null))
+        assertThat(SimpleJavaVersion(8, 2, 1)).isGreaterThan(SimpleJavaVersion(8, 1, 2))
+        assertThat(SimpleJavaVersion(8, 1, 2)).isLessThan(SimpleJavaVersion(8, 2, 1))
+        assertThat(SimpleJavaVersion(8, 1, null)).isEqualTo(SimpleJavaVersion(8, 1, null))
 
-        assert(SimpleJavaVersion(8, 1, 2) > SimpleJavaVersion(8, 1, 1))
-        assert(SimpleJavaVersion(8, 1, 1) < SimpleJavaVersion(8, 1, 2))
-        assert(SimpleJavaVersion(8, 1, 1) == SimpleJavaVersion(8, 1, 1))
+        assertThat(SimpleJavaVersion(8, 1, 2)).isGreaterThan(SimpleJavaVersion(8, 1, 1))
+        assertThat(SimpleJavaVersion(8, 1, 1)).isLessThan(SimpleJavaVersion(8, 1, 2))
+        assertThat(SimpleJavaVersion(8, 1, 1)).isEqualTo(SimpleJavaVersion(8, 1, 1))
 
-        assert(SimpleJavaVersion(8, 1, 0) > SimpleJavaVersion(8, 1, null))
-        assert(SimpleJavaVersion(8, 1, null) < SimpleJavaVersion(8, 1, 0))
+        assertThat(SimpleJavaVersion(8, 1, 0)).isGreaterThan(SimpleJavaVersion(8, 1, null))
+        assertThat(SimpleJavaVersion(8, 1, null)).isLessThan(SimpleJavaVersion(8, 1, 0))
     }
 }
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
new file mode 100644
index 0000000..8ad0e02
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.kotlintestapp.test
+
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.Update
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CopyOnWriteArrayList
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class QueryInterceptorTest {
+    @Rule
+    @JvmField
+    val countingTaskExecutorRule = CountingTaskExecutorRule()
+    lateinit var mDatabase: QueryInterceptorTestDatabase
+    var queryAndArgs = CopyOnWriteArrayList<Pair<String, ArrayList<Any>>>()
+
+    @Entity(tableName = "queryInterceptorTestDatabase")
+    data class QueryInterceptorEntity(@PrimaryKey val id: String, val description: String)
+
+    @Dao
+    interface QueryInterceptorDao {
+        @Query("DELETE FROM queryInterceptorTestDatabase WHERE id=:id")
+        fun delete(id: String)
+
+        @Insert
+        fun insert(item: QueryInterceptorEntity)
+
+        @Update
+        fun update(vararg item: QueryInterceptorEntity)
+    }
+
+    @Database(
+        version = 1,
+        entities = [
+            QueryInterceptorEntity::class
+        ],
+        exportSchema = false
+    )
+    abstract class QueryInterceptorTestDatabase : RoomDatabase() {
+        abstract fun queryInterceptorDao(): QueryInterceptorDao
+    }
+
+    @Before
+    fun setUp() {
+        mDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(),
+            QueryInterceptorTestDatabase::class.java
+        ).setQueryCallback(
+            RoomDatabase.QueryCallback { sqlQuery, bindArgs ->
+                val argTrace = ArrayList<Any>()
+                argTrace.addAll(bindArgs)
+                queryAndArgs.add(Pair(sqlQuery, argTrace))
+            },
+            MoreExecutors.directExecutor()
+        ).build()
+    }
+
+    @After
+    fun tearDown() {
+        mDatabase.close()
+    }
+
+    @Test
+    fun testInsert() {
+        mDatabase.queryInterceptorDao().insert(
+            QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+        )
+
+        assertQueryLogged(
+            "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                "VALUES (?,?)",
+            listOf("Insert", "Inserted a placeholder query")
+        )
+        assertTransactionQueries()
+    }
+
+    @Test
+    fun testDelete() {
+        mDatabase.queryInterceptorDao().delete("Insert")
+        assertQueryLogged(
+            "DELETE FROM queryInterceptorTestDatabase WHERE id=?",
+            listOf("Insert")
+        )
+        assertTransactionQueries()
+    }
+
+    @Test
+    fun testUpdate() {
+        mDatabase.queryInterceptorDao().insert(
+            QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+        )
+        mDatabase.queryInterceptorDao().update(
+            QueryInterceptorEntity("Insert", "Updated the placeholder query")
+        )
+
+        assertQueryLogged(
+            "UPDATE OR ABORT `queryInterceptorTestDatabase` SET `id` " +
+                "= ?,`description` = ? " +
+                "WHERE `id` = ?",
+            listOf("Insert", "Updated the placeholder query", "Insert")
+        )
+        assertTransactionQueries()
+    }
+
+    @Test
+    fun testCompileStatement() {
+        assertEquals(queryAndArgs.size, 0)
+        mDatabase.queryInterceptorDao().insert(
+            QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+        )
+        mDatabase.openHelper.writableDatabase.compileStatement(
+            "DELETE FROM queryInterceptorTestDatabase WHERE id=?"
+        ).execute()
+        assertQueryLogged("DELETE FROM queryInterceptorTestDatabase WHERE id=?", emptyList())
+    }
+
+    @Test
+    fun testLoggingSupportSQLiteQuery() {
+        mDatabase.openHelper.writableDatabase.query(
+            SimpleSQLiteQuery(
+                "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                    "VALUES (?,?)",
+                arrayOf<Any>("3", "Description")
+            )
+        )
+        assertQueryLogged(
+            "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                "VALUES (?,?)",
+            listOf("3", "Description")
+        )
+    }
+
+    @Test
+    fun testNullBindArgument() {
+        mDatabase.openHelper.writableDatabase.query(
+            SimpleSQLiteQuery(
+                "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                    "VALUES (?,?)",
+                arrayOf("ID", null)
+            )
+        )
+        assertQueryLogged(
+            "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`," +
+                "`description`) VALUES (?,?)",
+            listOf("ID", null)
+        )
+    }
+
+    @Test
+    fun testCallbackCalledOnceAfterCloseAndReOpen() {
+        val dbBuilder = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(),
+            QueryInterceptorTestDatabase::class.java
+        ).setQueryCallback(
+            RoomDatabase.QueryCallback { sqlQuery, bindArgs ->
+                val argTrace = ArrayList<Any>()
+                argTrace.addAll(bindArgs)
+                queryAndArgs.add(Pair(sqlQuery, argTrace))
+            },
+            MoreExecutors.directExecutor()
+        )
+
+        dbBuilder.build().close()
+
+        mDatabase = dbBuilder.build()
+
+        mDatabase.queryInterceptorDao().insert(
+            QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+        )
+
+        assertQueryLogged(
+            "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                "VALUES (?,?)",
+            listOf("Insert", "Inserted a placeholder query")
+        )
+        assertTransactionQueries()
+    }
+
+    private fun assertQueryLogged(
+        query: String,
+        expectedArgs: List<String?>
+    ) {
+        val filteredQueries = queryAndArgs.filter {
+            it.first == query
+        }
+        assertThat(filteredQueries).hasSize(1)
+        assertThat(expectedArgs).containsExactlyElementsIn(filteredQueries[0].second)
+    }
+
+    private fun assertTransactionQueries() {
+        assertNotNull(
+            queryAndArgs.any {
+                it.equals("BEGIN TRANSACTION")
+            }
+        )
+        assertNotNull(
+            queryAndArgs.any {
+                it.equals("TRANSACTION SUCCESSFUL")
+            }
+        )
+        assertNotNull(
+            queryAndArgs.any {
+                it.equals("END TRANSACTION")
+            }
+        )
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
index 4c0df94..94cccaf 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EnumColumnTypeAdapterTest.java
@@ -42,6 +42,7 @@
 public class EnumColumnTypeAdapterTest {
 
     private EnumColumnTypeAdapterDatabase mDb;
+    private EnumColumnTypeAdapterDatabase mDbComplex;
 
     @Entity
     public static class EntityWithEnum {
@@ -49,6 +50,12 @@
         public Long id;
         public Fruit fruit;
     }
+    @Entity
+    public static class ComplexEntityWithEnum {
+        @PrimaryKey
+        public Long id;
+        public Season mSeason;
+    }
 
     public enum Fruit {
         BANANA,
@@ -56,6 +63,19 @@
         WILDBERRY
     }
 
+    public enum Season {
+        SUMMER("Sunny"),
+        SPRING("Warm"),
+        WINTER("Cold"),
+        AUTUMN("Rainy");
+
+        private final String mSeason;
+
+        Season(String mSeason) {
+            this.mSeason = mSeason;
+        }
+    }
+
     @Dao
     public interface SampleDao {
         @Query("INSERT INTO EntityWithEnum (id, fruit) VALUES (:id, :fruit)")
@@ -65,9 +85,20 @@
         EntityWithEnum getValueWithId(long id);
     }
 
-    @Database(entities = {EntityWithEnum.class}, version = 1, exportSchema = false)
+    @Dao
+    public interface SampleDaoWithComplexEnum {
+        @Query("INSERT INTO ComplexEntityWithEnum (id, mSeason) VALUES (:id, :season)")
+        long insertComplex(long id, Season season);
+
+        @Query("SELECT * FROM ComplexEntityWithEnum WHERE id = :id")
+        ComplexEntityWithEnum getComplexValueWithId(long id);
+    }
+
+    @Database(entities = {EntityWithEnum.class, ComplexEntityWithEnum.class}, version = 1,
+            exportSchema = false)
     public abstract static class EnumColumnTypeAdapterDatabase extends RoomDatabase {
         public abstract EnumColumnTypeAdapterTest.SampleDao dao();
+        public abstract EnumColumnTypeAdapterTest.SampleDaoWithComplexEnum complexDao();
     }
 
     @Before
@@ -77,6 +108,10 @@
                 context,
                 EnumColumnTypeAdapterDatabase.class)
                 .build();
+        mDbComplex = Room.inMemoryDatabaseBuilder(
+                context,
+                EnumColumnTypeAdapterDatabase.class)
+                .build();
     }
 
     @Test
@@ -87,4 +122,11 @@
         assertThat(mDb.dao().getValueWithId(1).fruit, is(equalTo(Fruit.BANANA)));
         assertThat(mDb.dao().getValueWithId(2).fruit, is(equalTo(Fruit.STRAWBERRY)));
     }
+
+    @Test
+    public void filterOutComplexEnumTest() {
+        final long id1 = mDbComplex.complexDao().insertComplex(1, Season.AUTUMN);
+        assertThat(mDbComplex.complexDao().getComplexValueWithId(1).mSeason,
+                is(equalTo(Season.AUTUMN)));
+    }
 }
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index e77a927..061b0ad 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -87,6 +87,7 @@
     method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
     method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
     method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+    method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
   }
@@ -115,6 +116,10 @@
     method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
   }
 
+  public static interface RoomDatabase.QueryCallback {
+    method public void onQuery(String, java.util.List<java.lang.Object!>);
+  }
+
 }
 
 package androidx.room.migration {
diff --git a/room/runtime/api/public_plus_experimental_current.txt b/room/runtime/api/public_plus_experimental_current.txt
index a35e99f..e1cefb2 100644
--- a/room/runtime/api/public_plus_experimental_current.txt
+++ b/room/runtime/api/public_plus_experimental_current.txt
@@ -88,6 +88,7 @@
     method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
     method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
     method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+    method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
   }
@@ -116,6 +117,10 @@
     method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
   }
 
+  public static interface RoomDatabase.QueryCallback {
+    method public void onQuery(String, java.util.List<java.lang.Object!>);
+  }
+
 }
 
 package androidx.room.migration {
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index b875d71..4b2d12a 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -130,6 +130,7 @@
     method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
     method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
     method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+    method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
   }
@@ -158,6 +159,10 @@
     method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
   }
 
+  public static interface RoomDatabase.QueryCallback {
+    method public void onQuery(String, java.util.List<java.lang.Object!>);
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RoomOpenHelper extends androidx.sqlite.db.SupportSQLiteOpenHelper.Callback {
     ctor public RoomOpenHelper(androidx.room.DatabaseConfiguration, androidx.room.RoomOpenHelper.Delegate, String, String);
     ctor public RoomOpenHelper(androidx.room.DatabaseConfiguration, androidx.room.RoomOpenHelper.Delegate, String);
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java
new file mode 100644
index 0000000..c5ef6bc
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+
+/**
+ * Implements {@link SupportSQLiteDatabase} for SQLite queries.
+ */
+final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
+
+    private final SupportSQLiteDatabase mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final Executor mQueryCallbackExecutor;
+
+    QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase,
+            @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
+            queryCallbackExecutor) {
+        mDelegate = supportSQLiteDatabase;
+        mQueryCallback = queryCallback;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @NonNull
+    @Override
+    public SupportSQLiteStatement compileStatement(@NonNull String sql) {
+        return new QueryInterceptorStatement(mDelegate.compileStatement(sql),
+                mQueryCallback, sql, mQueryCallbackExecutor);
+    }
+
+    @Override
+    public void beginTransaction() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransaction();
+    }
+
+    @Override
+    public void beginTransactionNonExclusive() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransactionNonExclusive();
+    }
+
+    @Override
+    public void beginTransactionWithListener(@NonNull SQLiteTransactionListener
+            transactionListener) {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransactionWithListener(transactionListener);
+    }
+
+    @Override
+    public void beginTransactionWithListenerNonExclusive(
+            @NonNull SQLiteTransactionListener transactionListener) {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
+    }
+
+    @Override
+    public void endTransaction() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.endTransaction();
+    }
+
+    @Override
+    public void setTransactionSuccessful() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL",
+                Collections.emptyList()));
+        mDelegate.setTransactionSuccessful();
+    }
+
+    @Override
+    public boolean inTransaction() {
+        return mDelegate.inTransaction();
+    }
+
+    @Override
+    public boolean isDbLockedByCurrentThread() {
+        return mDelegate.isDbLockedByCurrentThread();
+    }
+
+    @Override
+    public boolean yieldIfContendedSafely() {
+        return mDelegate.yieldIfContendedSafely();
+    }
+
+    @Override
+    public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
+        return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
+    }
+
+    @Override
+    public int getVersion() {
+        return mDelegate.getVersion();
+    }
+
+    @Override
+    public void setVersion(int version) {
+        mDelegate.setVersion(version);
+    }
+
+    @Override
+    public long getMaximumSize() {
+        return mDelegate.getMaximumSize();
+    }
+
+    @Override
+    public long setMaximumSize(long numBytes) {
+        return mDelegate.setMaximumSize(numBytes);
+    }
+
+    @Override
+    public long getPageSize() {
+        return mDelegate.getPageSize();
+    }
+
+    @Override
+    public void setPageSize(long numBytes) {
+        mDelegate.setPageSize(numBytes);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull String query) {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
+                Collections.emptyList()));
+        return mDelegate.query(query);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) {
+        List<Object> inputArguments = new ArrayList<>();
+        inputArguments.addAll(Arrays.asList(bindArgs));
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
+                inputArguments));
+        return mDelegate.query(query, bindArgs);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull SupportSQLiteQuery query) {
+        QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
+        query.bindTo(queryInterceptorProgram);
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
+                queryInterceptorProgram.getBindArgs()));
+        return mDelegate.query(query);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull SupportSQLiteQuery query,
+            @NonNull CancellationSignal cancellationSignal) {
+        QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
+        query.bindTo(queryInterceptorProgram);
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
+                queryInterceptorProgram.getBindArgs()));
+        return mDelegate.query(query);
+    }
+
+    @Override
+    public long insert(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values)
+            throws SQLException {
+        return mDelegate.insert(table, conflictAlgorithm, values);
+    }
+
+    @Override
+    public int delete(@NonNull String table, @NonNull String whereClause,
+            @NonNull Object[] whereArgs) {
+        return mDelegate.delete(table, whereClause, whereArgs);
+    }
+
+    @Override
+    public int update(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values,
+            @NonNull String whereClause,
+            @NonNull Object[] whereArgs) {
+        return mDelegate.update(table, conflictAlgorithm, values, whereClause,
+                whereArgs);
+    }
+
+    @Override
+    public void execSQL(@NonNull String sql) throws SQLException {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, new ArrayList<>(0)));
+        mDelegate.execSQL(sql);
+    }
+
+    @Override
+    public void execSQL(@NonNull String sql, @NonNull Object[] bindArgs) throws SQLException {
+        List<Object> inputArguments = new ArrayList<>();
+        inputArguments.addAll(Arrays.asList(bindArgs));
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, inputArguments));
+        mDelegate.execSQL(sql, inputArguments.toArray());
+    }
+
+    @Override
+    public boolean isReadOnly() {
+        return mDelegate.isReadOnly();
+    }
+
+    @Override
+    public boolean isOpen() {
+        return mDelegate.isOpen();
+    }
+
+    @Override
+    public boolean needUpgrade(int newVersion) {
+        return mDelegate.needUpgrade(newVersion);
+    }
+
+    @NonNull
+    @Override
+    public String getPath() {
+        return mDelegate.getPath();
+    }
+
+    @Override
+    public void setLocale(@NonNull Locale locale) {
+        mDelegate.setLocale(locale);
+    }
+
+    @Override
+    public void setMaxSqlCacheSize(int cacheSize) {
+        mDelegate.setMaxSqlCacheSize(cacheSize);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public void setForeignKeyConstraintsEnabled(boolean enable) {
+        mDelegate.setForeignKeyConstraintsEnabled(enable);
+    }
+
+    @Override
+    public boolean enableWriteAheadLogging() {
+        return mDelegate.enableWriteAheadLogging();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public void disableWriteAheadLogging() {
+        mDelegate.disableWriteAheadLogging();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public boolean isWriteAheadLoggingEnabled() {
+        return mDelegate.isWriteAheadLoggingEnabled();
+    }
+
+    @NonNull
+    @Override
+    public List<Pair<String, String>> getAttachedDbs() {
+        return mDelegate.getAttachedDbs();
+    }
+
+    @Override
+    public boolean isDatabaseIntegrityOk() {
+        return mDelegate.isDatabaseIntegrityOk();
+    }
+
+    @Override
+    public void close() throws IOException {
+        mDelegate.close();
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java
new file mode 100644
index 0000000..c2ca486
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.concurrent.Executor;
+
+final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper {
+
+    private final SupportSQLiteOpenHelper mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final Executor mQueryCallbackExecutor;
+
+    QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
+            @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
+            queryCallbackExecutor) {
+        mDelegate = supportSQLiteOpenHelper;
+        mQueryCallback = queryCallback;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @Nullable
+    @Override
+    public String getDatabaseName() {
+        return mDelegate.getDatabaseName();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public void setWriteAheadLoggingEnabled(boolean enabled) {
+        mDelegate.setWriteAheadLoggingEnabled(enabled);
+    }
+
+    @Override
+    public SupportSQLiteDatabase getWritableDatabase() {
+        return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback,
+                mQueryCallbackExecutor);
+    }
+
+    @Override
+    public SupportSQLiteDatabase getReadableDatabase() {
+        return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback,
+                mQueryCallbackExecutor);
+    }
+
+    @Override
+    public void close() {
+        mDelegate.close();
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java
new file mode 100644
index 0000000..5d94cd1
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper.
+ */
+@SuppressWarnings("AcronymName")
+final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
+
+    private final SupportSQLiteOpenHelper.Factory mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final Executor mQueryCallbackExecutor;
+
+    @SuppressWarnings("LambdaLast")
+    QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory,
+            @NonNull RoomDatabase.QueryCallback queryCallback,
+            @NonNull Executor queryCallbackExecutor) {
+        mDelegate = factory;
+        mQueryCallback = queryCallback;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @NonNull
+    @Override
+    public SupportSQLiteOpenHelper create(
+            @NonNull SupportSQLiteOpenHelper.Configuration configuration) {
+        return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback,
+                mQueryCallbackExecutor);
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java
new file mode 100644
index 0000000..2b9c554
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import androidx.sqlite.db.SupportSQLiteProgram;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A program implementing an {@link SupportSQLiteProgram} API to record bind arguments.
+ */
+final class QueryInterceptorProgram implements SupportSQLiteProgram {
+    private List<Object> mBindArgsCache = new ArrayList<>();
+
+    @Override
+    public void bindNull(int index) {
+        saveArgsToCache(index, null);
+    }
+
+    @Override
+    public void bindLong(int index, long value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void bindDouble(int index, double value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void bindString(int index, String value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void bindBlob(int index, byte[] value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void clearBindings() {
+        mBindArgsCache.clear();
+    }
+
+    @Override
+    public void close() { }
+
+    private void saveArgsToCache(int bindIndex, Object value) {
+        // The index into bind methods are 1...n
+        int index = bindIndex - 1;
+        if (index >= mBindArgsCache.size()) {
+            for (int i = mBindArgsCache.size(); i <= index; i++) {
+                mBindArgsCache.add(null);
+            }
+        }
+        mBindArgsCache.set(index, value);
+    }
+
+    /**
+     * Returns the list of arguments associated with the query.
+     *
+     * @return argument list.
+     */
+    List<Object> getBindArgs() {
+        return mBindArgsCache;
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java
new file mode 100644
index 0000000..8825252
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements an instance of {@link SupportSQLiteStatement} for SQLite queries.
+ */
+final class QueryInterceptorStatement implements SupportSQLiteStatement {
+
+    private final SupportSQLiteStatement mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final String mSqlStatement;
+    private final List<Object> mBindArgsCache = new ArrayList<>();
+    private final Executor mQueryCallbackExecutor;
+
+    QueryInterceptorStatement(@NonNull SupportSQLiteStatement compileStatement,
+            @NonNull RoomDatabase.QueryCallback queryCallback, String sqlStatement,
+            @NonNull Executor queryCallbackExecutor) {
+        mDelegate = compileStatement;
+        mQueryCallback = queryCallback;
+        mSqlStatement = sqlStatement;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @Override
+    public void execute() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        mDelegate.execute();
+    }
+
+    @Override
+    public int executeUpdateDelete() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.executeUpdateDelete();
+    }
+
+    @Override
+    public long executeInsert() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.executeInsert();
+    }
+
+    @Override
+    public long simpleQueryForLong() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.simpleQueryForLong();
+    }
+
+    @Override
+    public String simpleQueryForString() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.simpleQueryForString();
+    }
+
+    @Override
+    public void bindNull(int index) {
+        saveArgsToCache(index, mBindArgsCache.toArray());
+        mDelegate.bindNull(index);
+    }
+
+    @Override
+    public void bindLong(int index, long value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindLong(index, value);
+    }
+
+    @Override
+    public void bindDouble(int index, double value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindDouble(index, value);
+    }
+
+    @Override
+    public void bindString(int index, String value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindString(index, value);
+    }
+
+    @Override
+    public void bindBlob(int index, byte[] value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindBlob(index, value);
+    }
+
+    @Override
+    public void clearBindings() {
+        mBindArgsCache.clear();
+        mDelegate.clearBindings();
+    }
+
+    @Override
+    public void close() throws IOException {
+        mDelegate.close();
+    }
+
+    private void saveArgsToCache(int bindIndex, Object value) {
+        int index = bindIndex - 1;
+        if (index >= mBindArgsCache.size()) {
+            // Add null entries to the list until we have the desired # of indices
+            for (int i = mBindArgsCache.size(); i <= index; i++) {
+                mBindArgsCache.add(null);
+            }
+        }
+        mBindArgsCache.set(index, value);
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index 7fbf181..4561876 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -620,6 +620,8 @@
         private final Context mContext;
         private ArrayList<Callback> mCallbacks;
         private PrepackagedDatabaseCallback mPrepackagedDatabaseCallback;
+        private QueryCallback mQueryCallback;
+        private Executor mQueryCallbackExecutor;
         private List<Object> mTypeConverters;
 
         /** The Executor used to run database queries. This should be background-threaded. */
@@ -1092,6 +1094,27 @@
         }
 
         /**
+         * Sets a {@link QueryCallback} to be invoked when queries are executed.
+         * <p>
+         * The callback is invoked whenever a query is executed, note that adding this callback
+         * has a small cost and should be avoided in production builds unless needed.
+         * <p>
+         * A use case for providing a callback is to allow logging executed queries. When the
+         * callback implementation logs then it is recommended to use an immediate executor.
+         *
+         * @param queryCallback The query callback.
+         * @param executor The executor on which the query callback will be invoked.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder<T> setQueryCallback(@NonNull QueryCallback queryCallback,
+                @NonNull Executor executor) {
+            mQueryCallback = queryCallback;
+            mQueryCallbackExecutor = executor;
+            return this;
+        }
+
+        /**
          * Adds a type converter instance to this database.
          *
          * @param typeConverter The converter. It must be an instance of a class annotated with
@@ -1149,8 +1172,12 @@
                 }
             }
 
+            SupportSQLiteOpenHelper.Factory factory;
+
             if (mFactory == null) {
-                mFactory = new FrameworkSQLiteOpenHelperFactory();
+                factory = new FrameworkSQLiteOpenHelperFactory();
+            } else {
+                factory = mFactory;
             }
 
             if (mCopyFromAssetPath != null
@@ -1170,14 +1197,20 @@
                             + "Builder, but the database can only be created using one of the "
                             + "three configurations.");
                 }
-                mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
-                        mCopyFromInputStream, mFactory);
+                factory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
+                        mCopyFromInputStream, factory);
             }
+
+            if (mQueryCallback != null) {
+                factory = new QueryInterceptorOpenHelperFactory(factory, mQueryCallback,
+                        mQueryCallbackExecutor);
+            }
+
             DatabaseConfiguration configuration =
                     new DatabaseConfiguration(
                             mContext,
                             mName,
-                            mFactory,
+                            factory,
                             mMigrationContainer,
                             mCallbacks,
                             mAllowMainThreadQueries,
@@ -1345,4 +1378,21 @@
         public void onOpenPrepackagedDatabase(@NonNull SupportSQLiteDatabase db) {
         }
     }
+
+    /**
+     * Callback interface for when SQLite queries are executed.
+     *
+     * @see RoomDatabase.Builder#setQueryCallback
+     */
+    public interface QueryCallback {
+
+        /**
+         * Called when a SQL query is executed.
+         *
+         * @param sqlQuery The SQLite query statement.
+         * @param bindArgs Arguments of the query if available, empty list otherwise.
+         */
+        void onQuery(@NonNull String sqlQuery, @NonNull List<Object>
+                bindArgs);
+    }
 }
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java
index 150e7a9..015d195 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java
@@ -20,10 +20,12 @@
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 
 import android.app.Activity;
+import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
 import android.text.Html;
 import android.view.View;
+import android.widget.Button;
 import android.widget.TextView;
 import android.widget.ToggleButton;
 
@@ -35,6 +37,7 @@
 import androidx.core.view.WindowInsetsCompat;
 
 import com.example.android.supportv4.R;
+import com.example.android.supportv4.graphics.DrawableCompatActivity;
 
 @SuppressWarnings("deprecation")
 public class WindowInsetsPlayground extends Activity {
@@ -69,6 +72,10 @@
             getWindow().setStatusBarColor(0x80000000);
             getWindow().setNavigationBarColor(0x80000000);
         }
+
+        Button newAct = findViewById(R.id.newAct);
+        newAct.setOnClickListener(
+                v -> startActivity(new Intent(this, DrawableCompatActivity.class)));
     }
 
     private void setRootWindowInsetsEnabled(boolean enabled) {
diff --git a/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml b/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml
index 3fdb522..d62dbbe 100644
--- a/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml
+++ b/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml
@@ -15,11 +15,13 @@
   -->
 
 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/insets_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fillViewport="true"
-    android:clipToPadding="false">
+    android:clipToPadding="false"
+    tools:ignore="HardcodedText">
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -52,6 +54,12 @@
                 android:textOn="View insets"
                 android:textOff="Root window insets" />
 
+            <Button
+                android:id="@+id/newAct"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="New Act" />
+
         </LinearLayout>
 
         <Space
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
index d16de8c..0a76af6 100644
--- a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
+++ b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
@@ -406,4 +406,32 @@
                 testValue);
     }
 
+    @Test
+    public void testReentrantCallbackCalls() throws Exception {
+        SharedPreferences encryptedSharedPreferences = EncryptedSharedPreferences
+                .create(mContext,
+                        PREFS_FILE,
+                        mMasterKey,
+                        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+                        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM);
+
+        encryptedSharedPreferences.registerOnSharedPreferenceChangeListener(
+                new SharedPreferences.OnSharedPreferenceChangeListener() {
+                    @Override
+                    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+                            String key) {
+                        sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
+                    }
+                });
+
+        encryptedSharedPreferences.registerOnSharedPreferenceChangeListener(
+                (sharedPreferences, key) -> {
+                    // No-op
+                });
+
+        SharedPreferences.Editor editor = encryptedSharedPreferences.edit();
+        editor.putString("someKey", "someValue");
+        editor.apply();
+    }
+
 }
diff --git a/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java b/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
index 6a231a6..44b327f 100644
--- a/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
+++ b/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
@@ -78,7 +78,7 @@
     private static final String NULL_VALUE = "__NULL__";
 
     final SharedPreferences mSharedPreferences;
-    final List<OnSharedPreferenceChangeListener> mListeners;
+    final CopyOnWriteArrayList<OnSharedPreferenceChangeListener> mListeners;
     final String mFileName;
     final String mMasterKeyAlias;
 
@@ -95,7 +95,7 @@
         mMasterKeyAlias = masterKeyAlias;
         mValueAead = aead;
         mKeyDeterministicAead = deterministicAead;
-        mListeners = new ArrayList<>();
+        mListeners = new CopyOnWriteArrayList<>();
     }
 
     /**
diff --git a/security/security-app-authenticator/OWNERS b/security/security-app-authenticator/OWNERS
new file mode 100644
index 0000000..fd24685
--- /dev/null
+++ b/security/security-app-authenticator/OWNERS
@@ -0,0 +1 @@
+mpgroover@google.com
\ No newline at end of file
diff --git a/security/security-app-authenticator/api/current.txt b/security/security-app-authenticator/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/security/security-app-authenticator/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/security/security-app-authenticator/api/public_plus_experimental_current.txt b/security/security-app-authenticator/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/security/security-app-authenticator/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutDirections.kt b/security/security-app-authenticator/api/res-current.txt
similarity index 100%
rename from compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/LayoutDirections.kt
rename to security/security-app-authenticator/api/res-current.txt
diff --git a/security/security-app-authenticator/api/restricted_current.txt b/security/security-app-authenticator/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/security/security-app-authenticator/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/security/security-app-authenticator/build.gradle b/security/security-app-authenticator/build.gradle
new file mode 100644
index 0000000..60e92d2
--- /dev/null
+++ b/security/security-app-authenticator/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    // Add dependencies here
+}
+
+androidx {
+    name = "Android Security App Package Authenitcator Library"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenVersion = LibraryVersions.SECURITY_APP_AUTHENTICATOR
+    mavenGroup = LibraryGroups.SECURITY
+    inceptionYear = "2020"
+    description = "Verify app packages for proper app to app authentication."
+}
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/security/security-app-authenticator/src/androidTest/AndroidManifest.xml
similarity index 92%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to security/security-app-authenticator/src/androidTest/AndroidManifest.xml
index 3bc2684..1ae9328 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/security/security-app-authenticator/src/androidTest/AndroidManifest.xml
@@ -15,5 +15,6 @@
   limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.car.app">
+    package="androidx.security.app.authenticator.test">
+
 </manifest>
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/security/security-app-authenticator/src/main/AndroidManifest.xml
similarity index 92%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to security/security-app-authenticator/src/main/AndroidManifest.xml
index 3bc2684..239ae64 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/security/security-app-authenticator/src/main/AndroidManifest.xml
@@ -15,5 +15,6 @@
   limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.car.app">
-</manifest>
+    package="androidx.security.app.authenticator">
+
+</manifest>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index c04ee3b..661f378 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -216,12 +216,14 @@
 includeProject(":compose:integration-tests:demos:common", "compose/integration-tests/demos/common", [BuildType.COMPOSE])
 includeProject(":compose:integration-tests:docs-snippets", "compose/integration-tests/docs-snippets", [BuildType.COMPOSE])
 includeProject(":compose:integration-tests:macrobenchmark", "compose/integration-tests/macrobenchmark", [BuildType.COMPOSE])
+includeProject(":compose:integration-tests:macrobenchmark-target", "compose/integration-tests/macrobenchmark-target", [BuildType.COMPOSE])
 includeProject(":compose:internal-lint-checks", "compose/internal-lint-checks", [BuildType.COMPOSE])
 includeProject(":compose:material", "compose/material", [BuildType.COMPOSE])
 includeProject(":compose:material:material", "compose/material/material", [BuildType.COMPOSE])
 includeProject(":compose:material:material-icons-core", "compose/material/material-icons-core", [BuildType.COMPOSE])
 includeProject(":compose:material:material-icons-core:material-icons-core-samples", "compose/material/material-icons-core/samples", [BuildType.COMPOSE])
 includeProject(":compose:material:material-icons-extended", "compose/material/material-icons-extended", [BuildType.COMPOSE])
+includeProject(":compose:material:material-ripple", "compose/material/material-ripple", [BuildType.COMPOSE])
 includeProject(":compose:material:material:icons:generator", "compose/material/material/icons/generator", [BuildType.COMPOSE])
 includeProject(":compose:material:material:integration-tests:material-demos", "compose/material/material/integration-tests/material-demos", [BuildType.COMPOSE])
 includeProject(":compose:material:material:integration-tests:material-studies", "compose/material/material/integration-tests/material-studies", [BuildType.COMPOSE])
@@ -430,6 +432,7 @@
 includeProject(":room:room-testing", "room/testing", [BuildType.MAIN])
 includeProject(":savedstate:savedstate", "savedstate/savedstate", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":savedstate:savedstate-ktx", "savedstate/savedstate-ktx", [BuildType.MAIN, BuildType.FLAN])
+includeProject(":security:security-app-authenticator", "security/security-app-authenticator", [BuildType.MAIN])
 includeProject(":security:security-biometric", "security/security-biometric", [BuildType.MAIN])
 includeProject(":security:security-crypto", "security/crypto", [BuildType.MAIN])
 includeProject(":security:security-crypto-ktx", "security/security-crypto-ktx", [BuildType.MAIN])
diff --git a/slices/core/api/restricted_current.txt b/slices/core/api/restricted_current.txt
index c1cada5..02fc647 100644
--- a/slices/core/api/restricted_current.txt
+++ b/slices/core/api/restricted_current.txt
@@ -5,6 +5,10 @@
     method public long currentTimeMillis();
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CornerDrawable extends android.graphics.drawable.InsetDrawable {
+    ctor public CornerDrawable(android.graphics.drawable.Drawable?, float);
+  }
+
   @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, isCustom=true) public final class Slice extends androidx.versionedparcelable.CustomVersionedParcelable implements androidx.versionedparcelable.VersionedParcelable {
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.Slice? bindSlice(android.content.Context!, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>!);
     method public java.util.List<java.lang.String!>! getHints();
diff --git a/slices/core/src/main/java/androidx/slice/CornerDrawable.java b/slices/core/src/main/java/androidx/slice/CornerDrawable.java
new file mode 100644
index 0000000..5afbe40
--- /dev/null
+++ b/slices/core/src/main/java/androidx/slice/CornerDrawable.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.slice;
+
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+ /**
+ * A wrapper for Drawables that uses a path to add mask for corners around the drawable,
+ * to match the radius of the underlying shape.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CornerDrawable extends InsetDrawable {
+    private float mCornerRadius;
+    private final Path mPath = new Path();
+
+    public CornerDrawable(@Nullable Drawable drawable, float cornerRadius) {
+        super(drawable, 0);
+        mCornerRadius = cornerRadius;
+    }
+
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        int saveCount = canvas.save();
+        canvas.clipPath(mPath);
+        super.draw(canvas);
+        canvas.restoreToCount(saveCount);
+    }
+
+    @Override
+    protected void onBoundsChange(@Nullable Rect r) {
+        if (mPath != null) {
+            mPath.reset();
+            mPath.addRoundRect(new RectF(r), mCornerRadius, mCornerRadius, Path.Direction.CW);
+        }
+        super.onBoundsChange(r);
+    }
+}
diff --git a/slices/test/lint-baseline.xml b/slices/test/lint-baseline.xml
index 7b41818..4dbd5e7 100644
--- a/slices/test/lint-baseline.xml
+++ b/slices/test/lint-baseline.xml
@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
+<issues format="5" by="lint 4.2.0-alpha16" client="gradle" variant="debug" version="4.2.0-alpha16">
 
     <issue
         id="ObsoleteLintCustomCheck"
         message="Lint found an issue registry (`androidx.annotation.experimental.lint.ExperimentalIssueRegistry`) which did not specify the Lint API version it was compiled with.&#xA;&#xA;**This means that the lint checks are likely not compatible.**&#xA;&#xA;If you are the author of this lint check, make your lint `IssueRegistry` class contain&#xA;  override val api: Int = com.android.tools.lint.detector.api.CURRENT_API&#xA;or from Java,&#xA;  @Override public int getApi() { return com.android.tools.lint.detector.api.ApiKt.CURRENT_API; }&#xA;&#xA;If you are just using lint checks from a third party library you have no control over, you can disable these lint checks (if they misbehave) like this:&#xA;&#xA;    android {&#xA;        lintOptions {&#xA;            disable &quot;UnsafeExperimentalUsageError&quot;,&#xA;                    &quot;UnsafeExperimentalUsageWarning&quot;&#xA;        }&#xA;    }&#xA;">
         <location
-            file="../../../../../.gradle/caches/transforms-2/files-2.1/cb45a5ae098b2615e2539f4e9a0ea73e/annotation-experimental-1.0.0/jars/lint.jar"/>
+            file="../../../../../.gradle/caches/transforms-2/files-2.1/597bdf06b2c6543399d6dc2389a6cda6/annotation-experimental-1.0.0/jars/lint.jar"/>
     </issue>
 
     <issue
@@ -59,7 +59,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="277"
+            line="283"
             column="26"/>
     </issue>
 
@@ -70,7 +70,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="321"
+            line="327"
             column="26"/>
     </issue>
 
@@ -81,7 +81,7 @@
         errorLine2="                                 ~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="364"
+            line="370"
             column="34"/>
     </issue>
 
@@ -92,7 +92,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="475"
+            line="543"
             column="26"/>
     </issue>
 
@@ -103,7 +103,7 @@
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="496"
+            line="564"
             column="25"/>
     </issue>
 
@@ -114,7 +114,7 @@
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="510"
+            line="578"
             column="25"/>
     </issue>
 
@@ -125,7 +125,7 @@
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="556"
+            line="624"
             column="25"/>
     </issue>
 
@@ -136,7 +136,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="603"
+            line="671"
             column="16"/>
     </issue>
 
@@ -147,11 +147,11 @@
         errorLine2="                                                                        ^">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="656"
+            line="724"
             column="73"/>
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="656"
+            line="724"
             column="73"/>
     </issue>
 
@@ -162,7 +162,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="666"
+            line="734"
             column="26"/>
     </issue>
 
@@ -173,7 +173,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="738"
+            line="806"
             column="26"/>
     </issue>
 
@@ -184,7 +184,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="766"
+            line="834"
             column="16"/>
     </issue>
 
@@ -195,7 +195,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="807"
+            line="875"
             column="16"/>
     </issue>
 
@@ -206,7 +206,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="894"
+            line="962"
             column="26"/>
     </issue>
 
@@ -217,7 +217,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="1103"
+            line="1171"
             column="16"/>
     </issue>
 
@@ -228,7 +228,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="1131"
+            line="1199"
             column="26"/>
     </issue>
 
@@ -239,7 +239,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="1193"
+            line="1261"
             column="26"/>
     </issue>
 
@@ -250,7 +250,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="1228"
+            line="1296"
             column="26"/>
     </issue>
 
@@ -261,7 +261,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="1316"
+            line="1384"
             column="16"/>
     </issue>
 
@@ -272,7 +272,7 @@
         errorLine2="                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="1406"
+            line="1474"
             column="19"/>
     </issue>
 
@@ -283,7 +283,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="1440"
+            line="1508"
             column="16"/>
     </issue>
 
@@ -396,6 +396,13 @@
 
     <issue
         id="IconLocation"
+        message="Found bitmap drawable `res/drawable/landscape.jpg` in densityless folder">
+        <location
+            file="src/main/res/drawable/landscape.jpg"/>
+    </issue>
+
+    <issue
+        id="IconLocation"
         message="Found bitmap drawable `res/drawable/mady.jpg` in densityless folder">
         <location
             file="src/main/res/drawable/mady.jpg"/>
@@ -445,6 +452,13 @@
 
     <issue
         id="IconLocation"
+        message="Found bitmap drawable `res/drawable/portrait.jpg` in densityless folder">
+        <location
+            file="src/main/res/drawable/portrait.jpg"/>
+    </issue>
+
+    <issue
+        id="IconLocation"
         message="Found bitmap drawable `res/drawable/reservation.png` in densityless folder">
         <location
             file="src/main/res/drawable/reservation.png"/>
@@ -513,7 +527,7 @@
         errorLine2="                  ~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="153"
+            line="155"
             column="19"/>
     </issue>
 
@@ -524,7 +538,7 @@
         errorLine2="                             ~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="153"
+            line="155"
             column="30"/>
     </issue>
 
@@ -535,7 +549,7 @@
         errorLine2="                                          ~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="153"
+            line="155"
             column="43"/>
     </issue>
 
@@ -546,7 +560,7 @@
         errorLine2="           ~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="173"
+            line="175"
             column="12"/>
     </issue>
 
@@ -557,7 +571,7 @@
         errorLine2="                  ~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/slice/test/SampleSliceProvider.java"
-            line="720"
+            line="788"
             column="19"/>
     </issue>
 
diff --git a/slices/test/src/main/java/androidx/slice/test/SampleSliceProvider.java b/slices/test/src/main/java/androidx/slice/test/SampleSliceProvider.java
index 9f87c3e..63b8c7a 100644
--- a/slices/test/src/main/java/androidx/slice/test/SampleSliceProvider.java
+++ b/slices/test/src/main/java/androidx/slice/test/SampleSliceProvider.java
@@ -111,6 +111,8 @@
             "contact4",
             "picker",
             "gallery",
+            "galleryimagesizemovie",
+            "galleryimagesizethumbnail",
             "galleryoverlay",
             "indeterminaterange",
             "indeterminaterange2",
@@ -218,6 +220,10 @@
                 return createPicker(sliceUri);
             case "/gallery":
                 return createGallery(sliceUri);
+            case "/galleryimagesizemovie":
+                return createGalleryImageSizeMovie(sliceUri);
+            case "/galleryimagesizethumbnail":
+                return createGalleryImageSizeThumbnails(sliceUri);
             case "/galleryoverlay":
                 return createGalleryOverlay(sliceUri);
             case "/weather":
@@ -415,6 +421,68 @@
         return lb.addGridRow(grb).build();
     }
 
+    private Slice createGalleryImageSizeMovie(Uri sliceUri) {
+        SliceAction primaryAction = SliceAction.create(
+                getBroadcastIntent(ACTION_TOAST, "open movie list"),
+                IconCompat.createWithResource(getContext(), R.drawable.slices_1),
+                LARGE_IMAGE,
+                "Open movie list");
+        ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY)
+                .setAccentColor(0xff4285F4);
+        lb.addRow(new RowBuilder()
+                .setTitle("These movies near you")
+                .setSubtitle("Top rated Movies")
+                .setPrimaryAction(primaryAction))
+                .addAction(SliceAction.create(
+                        getBroadcastIntent(ACTION_TOAST, ""),
+                        IconCompat.createWithResource(getContext(), R.drawable.ic_cast), ICON_IMAGE,
+                        "Share movie list"));
+        GridRowBuilder grb = new GridRowBuilder();
+        grb.addCell(new CellBuilder().addImage(IconCompat.createWithResource(getContext(),
+                R.drawable.portrait), RAW_IMAGE_LARGE)
+                .addTitleText("MovieName the movie"));
+        grb.addCell(new CellBuilder().addImage(IconCompat.createWithResource(getContext(),
+                R.drawable.portrait), RAW_IMAGE_LARGE)
+                .addTitleText("MovieName the movie2"));
+        grb.addCell(new CellBuilder().addImage(IconCompat.createWithResource(getContext(),
+                R.drawable.portrait), RAW_IMAGE_LARGE)
+                .addTitleText("Amazing movie"));
+        grb.addCell(new CellBuilder().addImage(IconCompat.createWithResource(getContext(),
+                R.drawable.portrait), RAW_IMAGE_LARGE)
+                .addTitleText("Cool movie").addText("The Sequel"));
+        return lb.addGridRow(grb).build();
+    }
+
+
+    private Slice createGalleryImageSizeThumbnails(Uri sliceUri) {
+        SliceAction primaryAction = SliceAction.create(
+                getBroadcastIntent(ACTION_TOAST, "open video list"),
+                IconCompat.createWithResource(getContext(), R.drawable.slices_1),
+                LARGE_IMAGE,
+                "Open video list");
+        ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY)
+                .setAccentColor(0xff4285F4);
+        lb.addRow(new RowBuilder()
+                .setTitle("Recommended videos")
+                .setSubtitle("Top rated Videos")
+                .setPrimaryAction(primaryAction))
+                .addAction(SliceAction.create(
+                        getBroadcastIntent(ACTION_TOAST, ""),
+                        IconCompat.createWithResource(getContext(), R.drawable.ic_cast), ICON_IMAGE,
+                        "Share videos"));
+        GridRowBuilder grb = new GridRowBuilder();
+        grb.addCell(new CellBuilder().addImage(IconCompat.createWithResource(getContext(),
+                R.drawable.landscape), RAW_IMAGE_LARGE)
+                .addTitleText("You won't believe this movie"));
+        grb.addCell(new CellBuilder().addImage(IconCompat.createWithResource(getContext(),
+                R.drawable.landscape), RAW_IMAGE_LARGE)
+                .addTitleText("Amazing movie"));
+        grb.addCell(new CellBuilder().addImage(IconCompat.createWithResource(getContext(),
+                R.drawable.landscape), RAW_IMAGE_LARGE)
+                .addTitleText("kittens wow"));
+        return lb.addGridRow(grb).build();
+    }
+
     private Slice createGalleryOverlay(Uri sliceUri) {
         SliceAction primaryAction = SliceAction.create(
                 getBroadcastIntent(ACTION_TOAST, "open photo album"),
diff --git a/slices/test/src/main/res/drawable/landscape.jpg b/slices/test/src/main/res/drawable/landscape.jpg
new file mode 100644
index 0000000..6941f05
--- /dev/null
+++ b/slices/test/src/main/res/drawable/landscape.jpg
Binary files differ
diff --git a/slices/test/src/main/res/drawable/portrait.jpg b/slices/test/src/main/res/drawable/portrait.jpg
new file mode 100644
index 0000000..38f5b71f
--- /dev/null
+++ b/slices/test/src/main/res/drawable/portrait.jpg
Binary files differ
diff --git a/slices/view/api/restricted_current.txt b/slices/view/api/restricted_current.txt
index d40654b..4e85c2a 100644
--- a/slices/view/api/restricted_current.txt
+++ b/slices/view/api/restricted_current.txt
@@ -122,6 +122,7 @@
     method public int getAccentColor();
     method public CharSequence? getContentDescription();
     method public androidx.slice.SliceItem? getContentIntent();
+    method public android.graphics.Point getFirstImageSize(android.content.Context);
     method public java.util.ArrayList<androidx.slice.widget.GridContent.CellContent!> getGridContent();
     method public int getHeight(androidx.slice.widget.SliceStyle!, androidx.slice.widget.SliceViewPolicy!);
     method public boolean getIsLastIndex();
@@ -143,10 +144,11 @@
     ctor public GridContent.CellContent(androidx.slice.SliceItem!);
     method public java.util.ArrayList<androidx.slice.SliceItem!> getCellItems();
     method public CharSequence? getContentDescription();
-    method public androidx.slice.SliceItem! getContentIntent();
+    method public androidx.slice.SliceItem? getContentIntent();
+    method public androidx.core.graphics.drawable.IconCompat? getImageIcon();
     method public int getImageMode();
     method public androidx.slice.SliceItem? getOverlayItem();
-    method public androidx.slice.SliceItem! getPicker();
+    method public androidx.slice.SliceItem? getPicker();
     method public int getTextCount();
     method public androidx.slice.SliceItem? getTitleItem();
     method public boolean hasImage();
diff --git a/slices/view/lint-baseline.xml b/slices/view/lint-baseline.xml
index 1199cdc7..999809f 100644
--- a/slices/view/lint-baseline.xml
+++ b/slices/view/lint-baseline.xml
@@ -1982,501 +1982,6 @@
     </issue>
 
     <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_min&quot; formatted=&quot;false&quot; msgid=&quot;6996334305156847955&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/strings.xml"
-            line="28"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/strings.xml"
-            line="30"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/strings.xml"
-            line="30"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_years&quot; formatted=&quot;false&quot; msgid=&quot;6212691832333991589&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/strings.xml"
-            line="30"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/strings.xml"
-            line="32"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/strings.xml"
-            line="36"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/strings.xml"
-            line="36"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;abc_slice_duration_days&quot; formatted=&quot;false&quot; msgid=&quot;6241698511167107334&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/strings.xml"
-            line="36"
-            column="5"/>
-    </issue>
-
-    <issue
         id="ObsoleteLayoutParam"
         message="Invalid layout param in a `LinearLayout`: `layout_alignStart`"
         errorLine1="            android:layout_alignStart=&quot;@android:id/title&quot;"
diff --git a/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java b/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java
index 3f98f01..8a2b761 100644
--- a/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java
@@ -48,6 +48,7 @@
 
     @Before
     public void setup() {
+        mContext.setTheme(R.style.AppTheme);
         // Empty XML file to initialize empty AttributeSet.
         XmlPullParser parser = mContext.getResources().getXml(R.xml.slice_style_test);
         AttributeSet attributes = Xml.asAttributeSet(parser);
diff --git a/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java b/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java
index f9dcce2..4fc92c0 100644
--- a/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java
@@ -74,6 +74,7 @@
     @Before
     @UiThreadTest
     public void setup() {
+        mContext.setTheme(R.style.AppTheme);
         mSliceView = new SliceView(mContext);
         SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
     }
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridContent.java b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
index 1fb968d..7aa346a 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
@@ -35,10 +35,15 @@
 import static androidx.slice.core.SliceHints.SUBTYPE_TIME_PICKER;
 import static androidx.slice.core.SliceHints.UNKNOWN_IMAGE;
 
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.core.graphics.drawable.IconCompat;
 import androidx.slice.SliceItem;
 import androidx.slice.SliceUtils;
 import androidx.slice.core.SliceActionImpl;
@@ -60,9 +65,10 @@
     private final ArrayList<CellContent> mGridContent = new ArrayList<>();
     private SliceItem mSeeMoreItem;
     private int mMaxCellLineCount;
-    private boolean mHasImage;
     private int mLargestImageMode = UNKNOWN_IMAGE;
     private boolean mIsLastIndex;
+    private IconCompat mFirstImage = null;
+    private Point mFirstImageSize = null;
 
     private SliceItem mTitleItem;
 
@@ -113,7 +119,9 @@
                 mAllImages = false;
             }
             mMaxCellLineCount = Math.max(mMaxCellLineCount, cc.getTextCount());
-            mHasImage |= cc.hasImage();
+            if (mFirstImage == null && cc.hasImage()) {
+                mFirstImage = cc.getImageIcon();
+            }
             mLargestImageMode = mLargestImageMode == UNKNOWN_IMAGE
                     ? cc.getImageMode()
                     : Math.max(mLargestImageMode, cc.getImageMode());
@@ -180,6 +188,21 @@
     }
 
     /**
+     * @return the first image dimensions in this row, if there are images.
+     */
+    @NonNull
+    public Point getFirstImageSize(@NonNull Context context) {
+        if (mFirstImage == null) {
+            return new Point(-1, -1);
+        }
+        if (mFirstImageSize == null) {
+            Drawable d = mFirstImage.loadDrawable(context);
+            mFirstImageSize = new Point(d.getIntrinsicWidth(), d.getIntrinsicHeight());
+        }
+        return mFirstImageSize;
+    }
+
+    /**
      * Filters non-cell items out of the list of items and finds content description.
      */
     private List<SliceItem> filterAndProcessItems(List<SliceItem> items) {
@@ -212,7 +235,7 @@
      * @return whether this row contains an image.
      */
     public boolean hasImage() {
-        return mHasImage;
+        return mFirstImage != null;
     }
 
     /**
@@ -244,7 +267,7 @@
         private final ArrayList<SliceItem> mCellItems = new ArrayList<>();
         private SliceItem mContentDescr;
         private int mTextCount;
-        private boolean mHasImage;
+        private IconCompat mImage;
         private SliceItem mOverlayItem;
         private int mImageMode = -1;
         private SliceItem mTitleItem;
@@ -300,7 +323,7 @@
                     } else if (imageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) {
                         mImageMode = SliceUtils.parseImageMode(item);
                         imageCount++;
-                        mHasImage = true;
+                        mImage = item.getIcon();
                         mCellItems.add(item);
                     }
                 }
@@ -329,6 +352,7 @@
         /**
          * @return the action to activate when this cell is tapped.
          */
+        @Nullable
         public SliceItem getContentIntent() {
             return mContentIntent;
         }
@@ -336,6 +360,7 @@
         /**
          * @return the Picker to use when this cell is tapped.
          */
+        @Nullable
         public SliceItem getPicker() {
             return mPicker;
         }
@@ -386,7 +411,7 @@
          * @return whether this cell contains an image.
          */
         public boolean hasImage() {
-            return mHasImage;
+            return mImage != null;
         }
 
         /**
@@ -396,6 +421,14 @@
             return mImageMode;
         }
 
+        /**
+         * @return the IconCompat of the image.
+         */
+        @Nullable
+        public IconCompat getImageIcon() {
+            return mImage;
+        }
+
         @Nullable
         public CharSequence getContentDescription() {
             return mContentDescr != null ? mContentDescr.getText() : null;
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
index 4ba719b..598cd88 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -30,6 +30,7 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import static androidx.slice.core.SliceHints.LARGE_IMAGE;
+import static androidx.slice.core.SliceHints.RAW_IMAGE_LARGE;
 import static androidx.slice.core.SliceHints.SUBTYPE_DATE_PICKER;
 import static androidx.slice.core.SliceHints.SUBTYPE_TIME_PICKER;
 import static androidx.slice.widget.EventInfo.ACTION_TYPE_DATE_PICK;
@@ -67,6 +68,7 @@
 import androidx.annotation.ColorInt;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.slice.CornerDrawable;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceHints;
 import androidx.slice.core.SliceQuery;
@@ -229,9 +231,17 @@
         }
         ArrayList<GridContent.CellContent> cells = mGridContent.getGridContent();
         if (cells.size() > 1) {
-            int desiredCellWidth = mGridContent.getLargestImageMode() == LARGE_IMAGE
-                    ? mLargeImageHeight
-                    : mSmallImageMinWidth;
+            int desiredCellWidth;
+            switch (mGridContent.getLargestImageMode()) {
+                case LARGE_IMAGE:
+                    desiredCellWidth = mLargeImageHeight;
+                    break;
+                case RAW_IMAGE_LARGE:
+                    desiredCellWidth = mGridContent.getFirstImageSize(getContext()).x;
+                    break;
+                default:
+                    desiredCellWidth = mSmallImageMinWidth;
+            }
             return getWidth() / (desiredCellWidth + mGutter);
         } else {
             return 1;
@@ -261,7 +271,8 @@
             mViewContainer.setContentDescription(contentDescr);
         }
         ArrayList<GridContent.CellContent> cells = mGridContent.getGridContent();
-        if (mGridContent.getLargestImageMode() == LARGE_IMAGE) {
+        if (mGridContent.getLargestImageMode() == LARGE_IMAGE
+                || mGridContent.getLargestImageMode() == RAW_IMAGE_LARGE) {
             mViewContainer.setGravity(Gravity.TOP);
         } else {
             mViewContainer.setGravity(Gravity.CENTER_VERTICAL);
@@ -469,6 +480,8 @@
     private boolean addImageItem(SliceItem item, SliceItem overlayItem, int color,
             ViewGroup container, boolean isSingle) {
         final String format = item.getFormat();
+        final boolean hasRoundedImage =
+                mSliceStyle != null && mSliceStyle.getApplyCornerRadiusToLargeImages();
         if (!FORMAT_IMAGE.equals(format) || item.getIcon() == null) {
             return false;
         }
@@ -477,13 +490,19 @@
             return false;
         }
         ImageView iv = new ImageView(getContext());
-        iv.setImageDrawable(d);
+        if (hasRoundedImage) {
+            CornerDrawable cd = new CornerDrawable(d, mSliceStyle.getImageCornerRadius());
+            iv.setImageDrawable(cd);
+        } else {
+            iv.setImageDrawable(d);
+        }
         LinearLayout.LayoutParams lp;
         if (item.hasHint(SliceHints.HINT_RAW)) {
             iv.setScaleType(ScaleType.CENTER_INSIDE);
-            lp = new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
+            lp = new LinearLayout.LayoutParams(mGridContent.getFirstImageSize(getContext()).x,
+                    mGridContent.getFirstImageSize(getContext()).y);
         } else if (item.hasHint(HINT_LARGE)) {
-            iv.setScaleType(ScaleType.CENTER_CROP);
+            iv.setScaleType(hasRoundedImage ? ScaleType.FIT_XY : ScaleType.CENTER_CROP);
             int height = isSingle ? MATCH_PARENT : mLargeImageHeight;
             lp = new LinearLayout.LayoutParams(MATCH_PARENT, height);
         } else {
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index b24ab5a..19922e3 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -17,6 +17,7 @@
 package androidx.slice.widget;
 
 import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
+import static android.app.slice.Slice.HINT_LARGE;
 import static android.app.slice.Slice.HINT_NO_TINT;
 import static android.app.slice.Slice.HINT_PARTIAL;
 import static android.app.slice.Slice.HINT_SHORTCUT;
@@ -86,6 +87,7 @@
 import androidx.core.graphics.drawable.DrawableCompat;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.core.view.ViewCompat;
+import androidx.slice.CornerDrawable;
 import androidx.slice.SliceItem;
 import androidx.slice.SliceStructure;
 import androidx.slice.core.SliceAction;
@@ -952,7 +954,14 @@
             final float density = getResources().getDisplayMetrics().density;
             ImageView iv = new ImageView(getContext());
             Drawable d = icon.loadDrawable(getContext());
-            iv.setImageDrawable(d);
+            final boolean hasRoundedImage =
+                    mSliceStyle != null && mSliceStyle.getApplyCornerRadiusToLargeImages();
+            if (hasRoundedImage && sliceItem.hasHint(HINT_LARGE)) {
+                CornerDrawable cd = new CornerDrawable(d, mSliceStyle.getImageCornerRadius());
+                iv.setImageDrawable(cd);
+            } else {
+                iv.setImageDrawable(d);
+            }
             if (isIcon && color != -1) {
                 iv.setColorFilter(color);
             }
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java b/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java
index 6432fa2..60917e0 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceStyle.java
@@ -17,6 +17,7 @@
 package androidx.slice.widget;
 
 import static androidx.slice.core.SliceHints.ICON_IMAGE;
+import static androidx.slice.core.SliceHints.RAW_IMAGE_LARGE;
 import static androidx.slice.core.SliceHints.UNKNOWN_IMAGE;
 import static androidx.slice.widget.SliceView.MODE_LARGE;
 import static androidx.slice.widget.SliceView.MODE_SMALL;
@@ -74,6 +75,7 @@
     private final int mGridBigPicMaxHeight;
     private final int mGridAllImagesHeight;
     private final int mGridImageTextHeight;
+    private final int mGridRawImageTextHeight;
     private final int mGridMaxHeight;
     private final int mGridMinHeight;
 
@@ -89,6 +91,8 @@
 
     private final Context mContext;
 
+    private final float mImageCornerRadius;
+
     public SliceStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
                 defStyleAttr, defStyleRes);
@@ -155,6 +159,8 @@
             mHideHeaderRow = a.getBoolean(R.styleable.SliceView_hideHeaderRow, false);
 
             mContext = context;
+
+            mImageCornerRadius = a.getDimension(R.styleable.SliceView_imageCornerRadius, 0);
         } finally {
             a.recycle();
         }
@@ -175,6 +181,8 @@
         mGridBigPicMaxHeight = r.getDimensionPixelSize(R.dimen.abc_slice_big_pic_max_height);
         mGridAllImagesHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_image_only_height);
         mGridImageTextHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_image_text_height);
+        mGridRawImageTextHeight = r.getDimensionPixelSize(
+                R.dimen.abc_slice_grid_raw_image_text_offset);
         mGridMinHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_min_height);
         mGridMaxHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_max_height);
 
@@ -304,6 +312,14 @@
         return mHideHeaderRow;
     }
 
+    public boolean getApplyCornerRadiusToLargeImages() {
+        return mImageCornerRadius > 0;
+    }
+
+    public float getImageCornerRadius() {
+        return mImageCornerRadius;
+    }
+
     public int getRowHeight(RowContent row, SliceViewPolicy policy) {
         int maxHeight = policy.getMaxSmallHeight() > 0 ? policy.getMaxSmallHeight() : mRowMaxHeight;
 
@@ -350,18 +366,30 @@
         int largestImageMode = grid.getLargestImageMode();
         int height;
         if (grid.isAllImages()) {
-            height = grid.getGridContent().size() == 1
-                    ? isSmall ? mGridBigPicMinHeight : mGridBigPicMaxHeight
-                    : largestImageMode == ICON_IMAGE ? mGridMinHeight
-                            : mGridAllImagesHeight;
+            height = (grid.getGridContent().size() == 1)
+                    ? (isSmall
+                    ? mGridBigPicMinHeight
+                    : mGridBigPicMaxHeight)
+                    : (largestImageMode == ICON_IMAGE
+                            ? mGridMinHeight
+                            : (largestImageMode == RAW_IMAGE_LARGE
+                                    ? grid.getFirstImageSize(mContext).y
+                                    : mGridAllImagesHeight));
         } else {
             boolean twoLines = grid.getMaxCellLineCount() > 1;
             boolean hasImage = grid.hasImage();
             boolean iconImagesOrNone = largestImageMode == ICON_IMAGE
                     || largestImageMode == UNKNOWN_IMAGE;
-            height = (twoLines && !isSmall)
-                    ? hasImage ? mGridMaxHeight : mGridMinHeight
-                    : iconImagesOrNone ? mGridMinHeight : mGridImageTextHeight;
+            height = largestImageMode == RAW_IMAGE_LARGE
+                    ? (grid.getFirstImageSize(mContext).y
+                        + (twoLines ? 2 : 1) * mGridRawImageTextHeight)
+                    : (twoLines && !isSmall)
+                            ? (hasImage
+                            ? mGridMaxHeight
+                            : mGridMinHeight)
+                            : (iconImagesOrNone
+                                    ? mGridMinHeight
+                                    : mGridImageTextHeight);
         }
         int topPadding = grid.isAllImages() && grid.getRowIndex() == 0
                 ? mGridTopPadding : 0;
diff --git a/slices/view/src/main/res/values/attrs.xml b/slices/view/src/main/res/values/attrs.xml
index 973e690..f0116cc 100644
--- a/slices/view/src/main/res/values/attrs.xml
+++ b/slices/view/src/main/res/values/attrs.xml
@@ -35,6 +35,9 @@
          a tint color here will override the app supplied color. -->
         <attr name="tintColor" />
 
+        <!-- The corner radius to be applied to each corner of large images. -->
+        <attr name="imageCornerRadius" format="dimension" />
+
         <!-- Text size to use for title text in the header of the slice. -->
         <attr name="headerTitleSize" format="dimension" />
         <!-- Text size to use for subtitle text in the header of the slice. -->
diff --git a/slices/view/src/main/res/values/dimens.xml b/slices/view/src/main/res/values/dimens.xml
index 1f0a371..e5b6e6c 100644
--- a/slices/view/src/main/res/values/dimens.xml
+++ b/slices/view/src/main/res/values/dimens.xml
@@ -61,6 +61,8 @@
     <!-- Height of a grid row displaying only large images; also used for min-width of large images
          to calculate # of images in a row -->
     <dimen name="abc_slice_grid_image_only_height">86dp</dimen>
+    <!-- Height of space to reserve for raw images with text-->
+    <dimen name="abc_slice_grid_raw_image_text_offset">30dp</dimen>
     <!-- Minimum width of a cell displaying an icon / small image; used to calculate #
          of images in a row -->
     <dimen name="abc_slice_grid_image_min_width">60dp</dimen>
diff --git a/studiow b/studiow
new file mode 100755
index 0000000..2dc12360
--- /dev/null
+++ b/studiow
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+function usage() {
+  echo "Usage: studiow [<project subset>]"
+  echo
+  echo "Project subsets:"
+  echo " m, main"
+  echo "  Open the project subset MAIN: non-Compose Jetpack libraries"
+  echo
+  echo " c, compose"
+  echo "  Open the project subset COMPOSE"
+  echo
+  echo " f, flan"
+  echo "  Open the project subset FLAN: Fragment, Lifecycle, Activity, and Navigation"
+  echo
+  echo " a, all"
+  echo "  Open the project subset ALL"
+  echo
+  exit 1
+}
+
+subsetArg="$1"
+if [ "$subsetArg" == "" ]; then
+  usage
+fi
+if [ "$subsetArg" == "m" -o "$subsetArg" == "main" ]; then
+  export ANDROIDX_PROJECTS=MAIN
+fi
+if [ "$subsetArg" == "c" -o "$subsetArg" == "compose" ]; then
+  export ANDROIDX_PROJECTS=COMPOSE
+fi
+if [ "$subsetArg" == "f" -o "$subsetArg" == "flan" ]; then
+  export ANDROIDX_PROJECTS=FLAN
+fi
+if [ "$subsetArg" == "a" -o "$subsetArg" == "all" ]; then
+  export ANDROIDX_PROJECTS=ALL
+fi
+if [ "$ANDROIDX_PROJECTS" == "" ]; then
+  echo "Unrecognized project argument: '$subsetArg'"
+  usage
+fi
+
+shift
+if [ "$1" != "" ]; then
+  echo "Unrecognized argument: '$1'"
+  usage
+fi
+source gradlew studio
diff --git a/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt b/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt
index f1730b0..f1ac548 100644
--- a/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt
+++ b/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt
@@ -20,10 +20,18 @@
 import java.util.concurrent.Executor
 
 class TestExecutor : Executor {
+    /**
+     * If true, adding a new task will drain all existing tasks.
+     */
+    var autoRun: Boolean = false
+
     private val mTasks = LinkedList<Runnable>()
 
     override fun execute(command: Runnable) {
         mTasks.add(command)
+        if (autoRun) {
+            executeAll()
+        }
     }
 
     fun executeAll(): Boolean {
diff --git a/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt b/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt
index 6e22616..76a4482 100644
--- a/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt
+++ b/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt
@@ -19,23 +19,6 @@
 import androidx.paging.LoadState.NotLoading
 
 /**
- * Converts a list of incremental LoadState updates to local source to a list of expected
- * [CombinedLoadStates] events.
- */
-@OptIn(ExperimentalStdlibApi::class)
-fun List<Pair<LoadType, LoadState>>.toCombinedLoadStatesLocal() = scan(
-    CombinedLoadStates(
-        source = LoadStates(
-            refresh = NotLoading(endOfPaginationReached = false),
-            prepend = NotLoading(endOfPaginationReached = false),
-            append = NotLoading(endOfPaginationReached = false)
-        )
-    )
-) { prev, update ->
-    prev.set(update.first, false, update.second)
-}
-
-/**
  * Test-only local-only LoadStates builder which defaults each state to [NotLoading], with
  * [LoadState.endOfPaginationReached] = `false`
  */
@@ -44,11 +27,14 @@
     prependLocal: LoadState = NotLoading(endOfPaginationReached = false),
     appendLocal: LoadState = NotLoading(endOfPaginationReached = false)
 ) = CombinedLoadStates(
+    refresh = refreshLocal,
+    prepend = prependLocal,
+    append = appendLocal,
     source = LoadStates(
         refresh = refreshLocal,
         prepend = prependLocal,
-        append = appendLocal
-    )
+        append = appendLocal,
+    ),
 )
 
 /**
@@ -56,6 +42,9 @@
  * [LoadState.endOfPaginationReached] = `false`
  */
 fun remoteLoadStatesOf(
+    refresh: LoadState = NotLoading(endOfPaginationReached = false),
+    prepend: LoadState = NotLoading(endOfPaginationReached = false),
+    append: LoadState = NotLoading(endOfPaginationReached = false),
     refreshLocal: LoadState = NotLoading(endOfPaginationReached = false),
     prependLocal: LoadState = NotLoading(endOfPaginationReached = false),
     appendLocal: LoadState = NotLoading(endOfPaginationReached = false),
@@ -63,66 +52,17 @@
     prependRemote: LoadState = NotLoading(endOfPaginationReached = false),
     appendRemote: LoadState = NotLoading(endOfPaginationReached = false)
 ) = CombinedLoadStates(
+    refresh = refresh,
+    prepend = prepend,
+    append = append,
     source = LoadStates(
         refresh = refreshLocal,
         prepend = prependLocal,
-        append = appendLocal
+        append = appendLocal,
     ),
     mediator = LoadStates(
         refresh = refreshRemote,
         prepend = prependRemote,
-        append = appendRemote
-    )
+        append = appendRemote,
+    ),
 )
-
-private fun CombinedLoadStates.set(
-    loadType: LoadType,
-    fromMediator: Boolean,
-    loadState: LoadState
-) = when (loadType) {
-    LoadType.REFRESH ->
-        if (fromMediator) {
-            copy(
-                mediator = mediator?.copy(refresh = loadState)
-                    ?: LoadStates(
-                        refresh = loadState,
-                        prepend = NotLoading(false),
-                        append = NotLoading(false)
-                    )
-            )
-        } else {
-            copy(
-                source = source.copy(refresh = loadState)
-            )
-        }
-    LoadType.PREPEND ->
-        if (fromMediator) {
-            copy(
-                mediator = mediator?.copy(prepend = loadState)
-                    ?: LoadStates(
-                        refresh = NotLoading(false),
-                        prepend = loadState,
-                        append = NotLoading(false)
-                    )
-            )
-        } else {
-            copy(
-                source = source.copy(prepend = loadState)
-            )
-        }
-    LoadType.APPEND ->
-        if (fromMediator) {
-            copy(
-                mediator = mediator?.copy(append = loadState)
-                    ?: LoadStates(
-                        refresh = NotLoading(false),
-                        prepend = NotLoading(false),
-                        append = loadState
-                    )
-            )
-        } else {
-            copy(
-                source = source.copy(append = loadState)
-            )
-        }
-}
diff --git a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt
index 7c329b9..014dae3 100644
--- a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt
+++ b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt
@@ -28,8 +28,12 @@
         previousList: NullPaddedList<T>,
         newList: NullPaddedList<T>,
         newCombinedLoadStates: CombinedLoadStates,
-        lastAccessedIndex: Int
-    ): Int? = null
+        lastAccessedIndex: Int,
+        onListPresentable: () -> Unit,
+    ): Int? {
+        onListPresentable()
+        return null
+    }
 
     companion object {
         private val noopDifferCallback = object : DifferCallback {
diff --git a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt
index d0113b5..d5f3b92 100644
--- a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt
+++ b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt
@@ -82,7 +82,6 @@
         }
     }
 
-    @OptIn(ExperimentalPagingApi::class)
     override fun getRefreshKey(state: PagingState<Int, Int>): Int? {
         getRefreshKeyCalls.add(state)
         return state.anchorPosition
diff --git a/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt b/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt
index b7feaa3..e52994e 100644
--- a/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt
+++ b/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt
@@ -127,7 +127,7 @@
         super.onActivityCreated(savedInstanceState)
         checkActivityNotDestroyed()
         calledOnActivityCreated = true
-        checkState("onActivityCreated", State.ATTACHED, State.CREATED)
+        checkState("onActivityCreated", State.ATTACHED, State.CREATED, State.VIEW_CREATED)
         val fromState = currentState
         currentState = State.ACTIVITY_CREATED
         onStateChanged(fromState)
@@ -198,6 +198,7 @@
         DETACHED,
         ATTACHED,
         CREATED,
+        VIEW_CREATED,
         ACTIVITY_CREATED,
         STARTED,
         RESUMED
diff --git a/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt b/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt
index 6e86816..1dfc738 100644
--- a/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt
+++ b/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt
@@ -46,3 +46,22 @@
 }
 
 fun fail(message: String? = null): Nothing = throw AssertionError(message)
+
+// The assertThrows above cannot be used from Java.
+@Suppress("UNCHECKED_CAST")
+fun <T : Throwable?> assertThrows(
+    expectedType: Class<T>,
+    runnable: Runnable
+): ThrowableSubject {
+    try {
+        runnable.run()
+    } catch (t: Throwable) {
+        if (expectedType.isInstance(t)) {
+            return assertThat(t)
+        }
+        throw t
+    }
+    throw AssertionError(
+        "Body completed successfully. Expected ${expectedType.simpleName}"
+    )
+}
\ No newline at end of file
diff --git a/testutils/testutils-truth/src/test/java/androidx/testutils/AssertionsTest.kt b/testutils/testutils-truth/src/test/java/androidx/testutils/AssertionsTest.kt
new file mode 100644
index 0000000..b65e977
--- /dev/null
+++ b/testutils/testutils-truth/src/test/java/androidx/testutils/AssertionsTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.testutils
+
+import org.junit.Test
+import java.io.IOException
+
+class AssertionsTest {
+    @Test
+    fun testNoFailureThrowsAssertionError() {
+        try {
+            assertThrows(IOException::class.java) {
+                // No Exception thrown
+            }
+        } catch (e: AssertionError) {
+            return // expected
+        }
+
+        fail("expected assertion error for no failure")
+    }
+
+    @Test
+    fun testIncorrectFailureThrowsAssertionError() {
+        try {
+            assertThrows(IOException::class.java) {
+                throw IllegalStateException()
+            }
+        } catch (e: IllegalStateException) {
+            return // expected
+        }
+
+        fail("expected IllegalStateException to propagate")
+    }
+
+    @Test
+    fun testCorrectFailureTypeIsCaughtAndReturnsAsThrowableSubject() {
+        assertThrows(IOException::class.java) {
+            throw IOException("test123")
+        }.hasMessageThat().contains("test123")
+    }
+}
\ No newline at end of file
diff --git a/wear/wear-complications-data/api/current.txt b/wear/wear-complications-data/api/current.txt
index 1a51768..15d192c 100644
--- a/wear/wear-complications-data/api/current.txt
+++ b/wear/wear-complications-data/api/current.txt
@@ -138,6 +138,13 @@
 
 package androidx.wear.complications {
 
+  public final class ComplicationBounds {
+    ctor public ComplicationBounds(java.util.Map<androidx.wear.complications.data.ComplicationType,? extends android.graphics.RectF> perComplicationTypeBounds);
+    ctor public ComplicationBounds(android.graphics.RectF bounds);
+    method public java.util.Map<androidx.wear.complications.data.ComplicationType,android.graphics.RectF> getPerComplicationTypeBounds();
+    property public final java.util.Map<androidx.wear.complications.data.ComplicationType,android.graphics.RectF> perComplicationTypeBounds;
+  }
+
   public final class ComplicationHelperActivity extends android.app.Activity implements androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback {
     ctor public ComplicationHelperActivity();
     method public static android.content.Intent createPermissionRequestHelperIntent(android.content.Context, android.content.ComponentName);
diff --git a/wear/wear-complications-data/api/public_plus_experimental_current.txt b/wear/wear-complications-data/api/public_plus_experimental_current.txt
index 8ee1e1c..abee040 100644
--- a/wear/wear-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/wear-complications-data/api/public_plus_experimental_current.txt
@@ -138,6 +138,13 @@
 
 package androidx.wear.complications {
 
+  public final class ComplicationBounds {
+    ctor public ComplicationBounds(java.util.Map<androidx.wear.complications.data.ComplicationType,? extends android.graphics.RectF> perComplicationTypeBounds);
+    ctor public ComplicationBounds(android.graphics.RectF bounds);
+    method public java.util.Map<androidx.wear.complications.data.ComplicationType,android.graphics.RectF> getPerComplicationTypeBounds();
+    property public final java.util.Map<androidx.wear.complications.data.ComplicationType,android.graphics.RectF> perComplicationTypeBounds;
+  }
+
   public final class ComplicationHelperActivity extends android.app.Activity implements androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback {
     ctor public ComplicationHelperActivity();
     method public static android.content.Intent createPermissionRequestHelperIntent(android.content.Context, android.content.ComponentName);
diff --git a/wear/wear-complications-data/api/restricted_current.txt b/wear/wear-complications-data/api/restricted_current.txt
index 16dcece..297a848 100644
--- a/wear/wear-complications-data/api/restricted_current.txt
+++ b/wear/wear-complications-data/api/restricted_current.txt
@@ -156,6 +156,13 @@
 
 package androidx.wear.complications {
 
+  public final class ComplicationBounds {
+    ctor public ComplicationBounds(java.util.Map<androidx.wear.complications.data.ComplicationType,? extends android.graphics.RectF> perComplicationTypeBounds);
+    ctor public ComplicationBounds(android.graphics.RectF bounds);
+    method public java.util.Map<androidx.wear.complications.data.ComplicationType,android.graphics.RectF> getPerComplicationTypeBounds();
+    property public final java.util.Map<androidx.wear.complications.data.ComplicationType,android.graphics.RectF> perComplicationTypeBounds;
+  }
+
   public final class ComplicationHelperActivity extends android.app.Activity implements androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback {
     ctor public ComplicationHelperActivity();
     method public static android.content.Intent createPermissionRequestHelperIntent(android.content.Context, android.content.ComponentName);
diff --git a/wear/wear-complications-data/lint-baseline.xml b/wear/wear-complications-data/lint-baseline.xml
index d0c3c4b..dde667a 100644
--- a/wear/wear-complications-data/lint-baseline.xml
+++ b/wear/wear-complications-data/lint-baseline.xml
@@ -45,994 +45,4 @@
             column="1"/>
     </issue>
 
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_days&quot; formatted=&quot;false&quot; msgid=&quot;8500262093840795448&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/complication_strings.xml"
-            line="4"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/complication_strings.xml"
-            line="8"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/complication_strings.xml"
-            line="10"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/complication_strings.xml"
-            line="10"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_hours&quot; formatted=&quot;false&quot; msgid=&quot;3258361256003469346&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/complication_strings.xml"
-            line="10"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/complication_strings.xml"
-            line="12"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/complication_strings.xml"
-            line="16"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/complication_strings.xml"
-            line="16"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_short_minutes&quot; formatted=&quot;false&quot; msgid=&quot;3812930575997556650&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/complication_strings.xml"
-            line="16"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/complication_strings.xml"
-            line="18"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/complication_strings.xml"
-            line="22"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/complication_strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/complication_strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_days&quot; formatted=&quot;false&quot; msgid=&quot;345557497041553025&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/complication_strings.xml"
-            line="24"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;in&quot; (Indonesian) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-in/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ja&quot; (Japanese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ja/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;km&quot; (Khmer) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-km/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ko&quot; (Korean) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ko/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lo&quot; (Lao) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lo/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;ms&quot; (Malay) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-ms/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;my&quot; (Burmese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-my/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;th&quot; (Thai) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-th/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;vi&quot; (Vietnamese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-vi/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rCN/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rHK/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;zh&quot; (Chinese) the following quantities are not relevant: `one`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-zh-rTW/complication_strings.xml"
-            line="26"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/complication_strings.xml"
-            line="30"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/complication_strings.xml"
-            line="30"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_hours&quot; formatted=&quot;false&quot; msgid=&quot;2990178439049007198&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/complication_strings.xml"
-            line="30"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;cs&quot; (Czech) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-cs/complication_strings.xml"
-            line="36"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;lt&quot; (Lithuanian) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-lt/complication_strings.xml"
-            line="36"
-            column="5"/>
-    </issue>
-
-    <issue
-        id="UnusedQuantity"
-        message="For language &quot;sk&quot; (Slovak) the following quantities are not relevant: `many`"
-        errorLine1="    &lt;plurals name=&quot;time_difference_words_minutes&quot; formatted=&quot;false&quot; msgid=&quot;9081188175463984403&quot;>"
-        errorLine2="    ^">
-        <location
-            file="src/main/res/values-sk/complication_strings.xml"
-            line="36"
-            column="5"/>
-    </issue>
-
 </issues>
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationBounds.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationBounds.kt
new file mode 100644
index 0000000..5f39c06
--- /dev/null
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationBounds.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.complications
+
+import android.graphics.RectF
+import androidx.wear.complications.data.ComplicationType
+
+/**
+ * ComplicationBounds are defined by fractional screen space coordinates in unit-square [0..1].
+ * These bounds will be subsequently clamped to the unit square and converted to screen space
+ * coordinates. NB 0 and 1 are included in the unit square.
+ *
+ * One bound is expected per [ComplicationType] to allow complications to change shape depending on
+ * the type.
+ */
+public class ComplicationBounds(
+    /** Per [ComplicationType] fractional unit-square screen space complication bounds. */
+    public val perComplicationTypeBounds: Map<ComplicationType, RectF>
+) {
+    /**
+     * Constructs a ComplicationBounds where all complication types have the same screen space
+     * unit-square bounds.
+     */
+    public constructor(bounds: RectF) : this(ComplicationType.values().associateWith { bounds })
+
+    init {
+        for (type in ComplicationType.values()) {
+            require(perComplicationTypeBounds.containsKey(type)) {
+                "Missing bounds for $type"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index 5409f896..c744f24 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -2,15 +2,17 @@
 package androidx.wear.watchface.client {
 
   public final class ComplicationState {
-    ctor public ComplicationState(android.graphics.Rect bounds, int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled);
+    ctor public ComplicationState(android.graphics.Rect bounds, int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled, androidx.wear.complications.data.ComplicationType currentType);
     method public android.graphics.Rect getBounds();
     method public int getBoundsType();
+    method public androidx.wear.complications.data.ComplicationType getCurrentType();
     method public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
     method public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
     method public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
     method public boolean isEnabled();
     property public final android.graphics.Rect bounds;
     property public final int boundsType;
+    property public final androidx.wear.complications.data.ComplicationType currentType;
     property public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
     property public final androidx.wear.complications.data.ComplicationType defaultProviderType;
     property public final boolean isEnabled;
@@ -35,8 +37,8 @@
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +58,7 @@
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
     method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
@@ -84,13 +86,16 @@
 
   public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
     method public android.os.IBinder asBinder();
+    method public void bringAttentionToComplication(int complicationId);
     method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
+    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public String getInstanceId();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 56fd228..3e7223f 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -2,15 +2,17 @@
 package androidx.wear.watchface.client {
 
   public final class ComplicationState {
-    ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled);
+    ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled, androidx.wear.complications.data.ComplicationType currentType);
     method public android.graphics.Rect getBounds();
     method public int getBoundsType();
+    method public androidx.wear.complications.data.ComplicationType getCurrentType();
     method public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
     method public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
     method public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
     method public boolean isEnabled();
     property public final android.graphics.Rect bounds;
     property public final int boundsType;
+    property public final androidx.wear.complications.data.ComplicationType currentType;
     property public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
     property public final androidx.wear.complications.data.ComplicationType defaultProviderType;
     property public final boolean isEnabled;
@@ -35,8 +37,8 @@
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +58,7 @@
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
     method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
@@ -84,13 +86,16 @@
 
   public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
     method public android.os.IBinder asBinder();
+    method public void bringAttentionToComplication(int complicationId);
     method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
+    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public String getInstanceId();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index 2870cb3..7961108 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -2,15 +2,17 @@
 package androidx.wear.watchface.client {
 
   public final class ComplicationState {
-    ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled);
+    ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled, androidx.wear.complications.data.ComplicationType currentType);
     method public android.graphics.Rect getBounds();
     method public int getBoundsType();
+    method public androidx.wear.complications.data.ComplicationType getCurrentType();
     method public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
     method public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
     method public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
     method public boolean isEnabled();
     property public final android.graphics.Rect bounds;
     property public final int boundsType;
+    property public final androidx.wear.complications.data.ComplicationType currentType;
     property public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
     property public final androidx.wear.complications.data.ComplicationType defaultProviderType;
     property public final boolean isEnabled;
@@ -35,8 +37,8 @@
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +58,7 @@
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, @androidx.wear.watchface.client.TapType int tapType);
     method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
@@ -84,13 +86,16 @@
 
   public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
     method public android.os.IBinder asBinder();
+    method public void bringAttentionToComplication(int complicationId);
     method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
+    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public String getInstanceId();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/build.gradle b/wear/wear-watchface-client/build.gradle
index 4b4eaefa..b5c1d3f 100644
--- a/wear/wear-watchface-client/build.gradle
+++ b/wear/wear-watchface-client/build.gradle
@@ -51,7 +51,7 @@
 
 android {
     defaultConfig {
-        minSdkVersion 27
+        minSdkVersion 25
     }
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/wear/wear-watchface-client"
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 96889ba..898a8cfe 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -19,6 +19,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Color
 import android.graphics.Rect
 import android.os.Handler
 import android.os.Looper
@@ -32,20 +33,31 @@
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationText
 import androidx.wear.complications.data.ComplicationType
+import androidx.wear.complications.data.LongTextComplicationData
 import androidx.wear.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.DrawMode
+import androidx.wear.watchface.LayerMode
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.client.DeviceConfig
 import androidx.wear.watchface.client.SystemState
 import androidx.wear.watchface.client.WatchFaceControlClientImpl
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.data.ComplicationBoundsType
+import androidx.wear.watchface.samples.BLUE_STYLE
+import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
+import androidx.wear.watchface.samples.COMPLICATIONS_STYLE_SETTING
+import androidx.wear.watchface.samples.DRAW_HOUR_PIPS_STYLE_SETTING
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.GREEN_STYLE
+import androidx.wear.watchface.samples.NO_COMPLICATIONS
+import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
+import androidx.wear.watchface.style.Layer
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
@@ -144,7 +156,12 @@
             400
         ).get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
         val bitmap = headlessInstance.takeWatchFaceScreenshot(
-            RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                RenderParameters.DRAW_ALL_LAYERS,
+                null,
+                Color.RED
+            ),
             100,
             1234567,
             null,
@@ -157,6 +174,44 @@
     }
 
     @Test
+    fun yellowComplicationHighlights() {
+        val headlessInstance = service.createHeadlessWatchFaceClient(
+            ComponentName(
+                "androidx.wear.watchface.samples.test",
+                "androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService"
+            ),
+            DeviceConfig(
+                false,
+                false,
+                0,
+                0
+            ),
+            400,
+            400
+        ).get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+        val bitmap = headlessInstance.takeWatchFaceScreenshot(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                mapOf(
+                    Layer.BASE_LAYER to LayerMode.DRAW,
+                    Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
+                    Layer.TOP_LAYER to LayerMode.DRAW
+                ),
+                null,
+                Color.YELLOW
+            ),
+            100,
+            1234567,
+            null,
+            complications
+        )
+
+        bitmap.assertAgainstGolden(screenshotRule, "yellowComplicationHighlights")
+
+        headlessInstance.close()
+    }
+
+    @Test
     fun headlessComplicationDetails() {
         val headlessInstance = service.createHeadlessWatchFaceClient(
             exampleWatchFaceComponentName,
@@ -257,7 +312,12 @@
             interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
 
         val bitmap = interactiveInstance.takeWatchFaceScreenshot(
-            RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                RenderParameters.DRAW_ALL_LAYERS,
+                null,
+                Color.RED
+            ),
             100,
             1234567,
             null,
@@ -297,7 +357,12 @@
             interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
 
         val bitmap = interactiveInstance.takeWatchFaceScreenshot(
-            RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                RenderParameters.DRAW_ALL_LAYERS,
+                null,
+                Color.RED
+            ),
             100,
             1234567,
             null,
@@ -331,6 +396,15 @@
         val interactiveInstance =
             interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
 
+        interactiveInstance.updateComplicationData(
+            mapOf(
+                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
+                    ShortTextComplicationData.Builder(ComplicationText.plain("Test")).build(),
+                EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to
+                    LongTextComplicationData.Builder(ComplicationText.plain("Test")).build()
+            )
+        )
+
         assertThat(interactiveInstance.complicationState.size).isEqualTo(2)
 
         val leftComplicationDetails = interactiveInstance.complicationState[
@@ -352,6 +426,9 @@
             ComplicationType.SMALL_IMAGE
         )
         assertTrue(leftComplicationDetails.isEnabled)
+        assertThat(leftComplicationDetails.currentType).isEqualTo(
+            ComplicationType.SHORT_TEXT
+        )
 
         val rightComplicationDetails = interactiveInstance.complicationState[
             EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
@@ -372,6 +449,9 @@
             ComplicationType.SMALL_IMAGE
         )
         assertTrue(rightComplicationDetails.isEnabled)
+        assertThat(rightComplicationDetails.currentType).isEqualTo(
+            ComplicationType.LONG_TEXT
+        )
 
         interactiveInstance.close()
     }
@@ -486,6 +566,92 @@
         assertThat(contentDescriptionLabels[2].getTextAt(context.resources, 0))
             .isEqualTo("ID Right")
     }
+
+    @Test
+    fun setUserStyle() {
+        val interactiveInstanceFuture =
+            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+                "testId",
+                deviceConfig,
+                systemState,
+                mapOf(
+                    COLOR_STYLE_SETTING to GREEN_STYLE,
+                    WATCH_HAND_LENGTH_STYLE_SETTING to "0.25",
+                    DRAW_HOUR_PIPS_STYLE_SETTING to "false",
+                    COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS
+                ),
+                complications
+            )
+
+        Mockito.`when`(surfaceHolder.surfaceFrame)
+            .thenReturn(Rect(0, 0, 400, 400))
+
+        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
+        createEngine()
+
+        // Wait for the instance to be created.
+        val interactiveInstance =
+            interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+
+        // Note this map doesn't include all the categories, which is fine the others will be set
+        // to their defaults.
+        interactiveInstance.setUserStyle(
+            mapOf(
+                COLOR_STYLE_SETTING to BLUE_STYLE,
+                WATCH_HAND_LENGTH_STYLE_SETTING to "0.9",
+            )
+        )
+
+        val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                RenderParameters.DRAW_ALL_LAYERS,
+                null,
+                Color.RED
+            ),
+            100,
+            1234567,
+            null,
+            complications
+        )
+
+        try {
+            // Note the hour hand pips and both complications should be visible in this image.
+            bitmap.assertAgainstGolden(screenshotRule, "setUserStyle")
+        } finally {
+            interactiveInstance.close()
+        }
+    }
+
+    @Test
+    fun getComplicationIdAt() {
+        val interactiveInstanceFuture =
+            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+                "testId",
+                deviceConfig,
+                systemState,
+                null,
+                complications
+            )
+
+        Mockito.`when`(surfaceHolder.surfaceFrame)
+            .thenReturn(Rect(0, 0, 400, 400))
+
+        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
+        createEngine()
+
+        val interactiveInstance =
+            interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+
+        assertNull(interactiveInstance.getComplicationIdAt(0, 0))
+        assertThat(interactiveInstance.getComplicationIdAt(85, 165)).isEqualTo(
+            EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+        )
+        assertThat(interactiveInstance.getComplicationIdAt(255, 165)).isEqualTo(
+            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+        )
+        interactiveInstance.close()
+    }
 }
 
 internal class TestExampleCanvasAnalogWatchFaceService(
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
index 889a3a5..5c2dda8 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.Rect
 import androidx.wear.complications.DefaultComplicationProviderPolicy
+import androidx.wear.complications.data.ComplicationData
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.data.ComplicationBoundsType
@@ -31,7 +32,7 @@
     /** The type of the complication's bounds. */
     @ComplicationBoundsType public val boundsType: Int,
 
-    /** The [ComplicationType]s supported for this complication. */
+    /** The [ComplicationType]s supported by this complication. */
     public val supportedTypes: List<ComplicationType>,
 
     /** The [DefaultComplicationProviderPolicy] for this complication. */
@@ -42,7 +43,10 @@
 
     /** Whether or not the complication is drawn. */
     @get:JvmName("isEnabled")
-    public val isEnabled: Boolean
+    public val isEnabled: Boolean,
+
+    /** The [ComplicationType] of the complication's current [ComplicationData]. */
+    public val currentType: ComplicationType
 ) {
     internal constructor(
         complicationStateWireFormat: ComplicationStateWireFormat
@@ -55,6 +59,7 @@
             complicationStateWireFormat.fallbackSystemProvider
         ),
         ComplicationType.fromWireType(complicationStateWireFormat.defaultProviderType),
-        complicationStateWireFormat.isEnabled
+        complicationStateWireFormat.isEnabled,
+        ComplicationType.fromWireType(complicationStateWireFormat.currentType)
     )
 }
\ No newline at end of file
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
index e432cc0..521ef55 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
@@ -20,6 +20,7 @@
 import android.os.IBinder
 import android.support.wearable.watchface.SharedMemoryImage
 import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.control.IHeadlessWatchFace
@@ -29,7 +30,13 @@
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSchema
 
-/** Controls a stateless remote headless watch face. */
+/**
+ * Controls a stateless remote headless watch face.  This is mostly intended for use by watch face
+ * editor UIs which need to generate screenshots for various styling configurations without
+ * affecting the current watchface.
+ *
+ * Note clients should call [close] when finished.
+ */
 public interface HeadlessWatchFaceClient : AutoCloseable {
     /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
     public val previewReferenceTimeMillis: Long
@@ -63,6 +70,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings.
      */
+    @RequiresApi(27)
     public fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -85,6 +93,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings, or `null` if [complicationId] is unrecognized.
      */
+    @RequiresApi(27)
     public fun takeComplicationScreenshot(
         complicationId: Int,
         renderParameters: RenderParameters,
@@ -117,6 +126,7 @@
             { ComplicationState(it.complicationState) }
         )
 
+    @RequiresApi(27)
     override fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -141,6 +151,7 @@
         )
     )
 
+    @RequiresApi(27)
     override fun takeComplicationScreenshot(
         complicationId: Int,
         renderParameters: RenderParameters,
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
index 10b2b1b..5d41c43 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
@@ -25,6 +25,7 @@
 import android.support.wearable.watchface.SharedMemoryImage
 import androidx.annotation.IntDef
 import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.watchface.RenderParameters
@@ -49,7 +50,9 @@
 
 /**
  * Controls a stateful remote interactive watch face with an interface tailored for SysUI the
- * WearOS 3.0 launcher app. Typically this will be used for the current active watch face.
+ * WearOS launcher app. Typically this will be used for the current active watch face.
+ *
+ * Note clients should call [close] when finished.
  */
 public interface InteractiveWatchFaceSysUiClient : AutoCloseable {
 
@@ -71,7 +74,10 @@
          */
         public const val TAP_TYPE_TAP: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TAP
 
-        /** Constructs a [InteractiveWatchFaceSysUiClient] from an [IBinder]. */
+        /**
+         * Constructs an [InteractiveWatchFaceSysUiClient] from the [IBinder] returned by
+         * [asBinder].
+         */
         @JvmStatic
         public fun createFromBinder(binder: IBinder): InteractiveWatchFaceSysUiClient =
             InteractiveWatchFaceSysUiClientImpl(binder)
@@ -136,6 +142,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings.
      */
+    @RequiresApi(27)
     public fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -181,6 +188,7 @@
                 )
             }
 
+    @RequiresApi(27)
     override fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
index 71e7ecf..65d2a76 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
@@ -20,23 +20,31 @@
 import android.os.IBinder
 import android.support.wearable.watchface.SharedMemoryImage
 import androidx.annotation.IntRange
+import androidx.annotation.Px
+import androidx.annotation.RequiresApi
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
 import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.data.UserStyleWireFormat
 
 /**
  * Controls a stateful remote interactive watch face with an interface tailored for WCS the
- * WearOS 3.0 system server responsible for watch face management. Typically this will be used for
+ * WearOS system server responsible for watch face management. Typically this will be used for
  * the current active watch face.
+ *
+ * Note clients should call [close] when finished.
  */
 public interface InteractiveWatchFaceWcsClient : AutoCloseable {
 
     public companion object {
-        /** Constructs a [InteractiveWatchFaceWcsClient] from an [IBinder]. */
+        /**
+         * Constructs an [InteractiveWatchFaceWcsClient] from the [IBinder] returned by [asBinder].
+         */
         @JvmStatic
         public fun createFromBinder(binder: IBinder): InteractiveWatchFaceWcsClient =
             InteractiveWatchFaceWcsClientImpl(binder)
@@ -62,6 +70,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings.
      */
+    @RequiresApi(27)
     public fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -79,6 +88,13 @@
      */
     public fun setUserStyle(userStyle: UserStyle)
 
+    /**
+     * Sets the watch face's current UserStyle represented as a Map<String, String>.  This can be
+     * helpful to avoid having to construct a [UserStyle] which requires the [UserStyleSchema]
+     * which is an additional IPC. Note this may alter [complicationState].
+     */
+    public fun setUserStyle(userStyle: Map<String, String>)
+
     /** Returns the ID of this watch face instance. */
     public val instanceId: String
 
@@ -93,6 +109,24 @@
 
     /** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
     public fun asBinder(): IBinder
+
+    /** Returns the ID of the complication at the given coordinates or `null` if there isn't one.*/
+    @SuppressWarnings("AutoBoxing")
+    public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
+        complicationState.asSequence().firstOrNull {
+            it.value.isEnabled && when (it.value.boundsType) {
+                ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
+                ComplicationBoundsType.BACKGROUND -> false
+                ComplicationBoundsType.EDGE -> false
+                else -> false
+            }
+        }?.key
+
+    /**
+     * Requests the specified complication is highlighted for a short period to bring attention to
+     * it.
+     */
+    public fun bringAttentionToComplication(complicationId: Int)
 }
 
 /** Controls a stateful remote interactive watch face with an interface tailored for WCS. */
@@ -110,6 +144,7 @@
         )
     }
 
+    @RequiresApi(27)
     override fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -141,6 +176,10 @@
         iInteractiveWatchFaceWcs.setCurrentUserStyle(userStyle.toWireFormat())
     }
 
+    override fun setUserStyle(userStyle: Map<String, String>) {
+        iInteractiveWatchFaceWcs.setCurrentUserStyle(UserStyleWireFormat(userStyle))
+    }
+
     override val instanceId: String
         get() = iInteractiveWatchFaceWcs.instanceId
 
@@ -158,4 +197,8 @@
     }
 
     override fun asBinder(): IBinder = iInteractiveWatchFaceWcs.asBinder()
+
+    override fun bringAttentionToComplication(complicationId: Int) {
+        iInteractiveWatchFaceWcs.bringAttentionToComplication(complicationId)
+    }
 }
\ No newline at end of file
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
index e051212..d3498be 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
@@ -35,13 +35,17 @@
 import com.google.common.util.concurrent.ListenableFuture
 
 /**
- * Connects to a watch face's WatchFaceControlService which allows the user to control the
- * watch face.
+ * Connects to a watch face's WatchFaceControlService which allows the user to control the watch
+ * face.
  */
 public interface WatchFaceControlClient : AutoCloseable {
 
     public companion object {
-        /** Constructs a client which connects to a watch face in the given android package. */
+        /**
+         * Constructs a [WatchFaceControlClient] which attempts to connect to a watch face in the
+         * android package [watchFacePackageName]. If this fails the [ListenableFuture]s returned by
+         * WatchFaceControlClient methods will fail with [ServiceNotBoundException].
+         */
         @JvmStatic
         public fun createWatchFaceControlClient(
             /** Calling application's [Context]. */
diff --git a/wear/wear-watchface-complications-rendering/build.gradle b/wear/wear-watchface-complications-rendering/build.gradle
index 60b135a..2735d4b 100644
--- a/wear/wear-watchface-complications-rendering/build.gradle
+++ b/wear/wear-watchface-complications-rendering/build.gradle
@@ -54,7 +54,7 @@
         aidl = true
     }
     defaultConfig {
-        minSdkVersion 26
+        minSdkVersion 25
     }
 
     // Use Robolectric 4.+
diff --git a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
index 95e4126..91c08a4 100644
--- a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
+++ b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
@@ -34,7 +34,6 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
@@ -51,8 +50,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.wear.complications.ComplicationHelperActivity;
+import androidx.wear.watchface.CanvasType;
 import androidx.wear.watchface.ComplicationsManager;
-import androidx.wear.watchface.RenderParameters;
 import androidx.wear.watchface.Renderer;
 import androidx.wear.watchface.WatchFace;
 import androidx.wear.watchface.WatchFaceService;
@@ -720,18 +719,13 @@
                     WatchFaceType.ANALOG,
                     userStyleRepository,
                     new ComplicationsManager(new ArrayList<>(), userStyleRepository),
-                    new Renderer(surfaceHolder, userStyleRepository, watchState, 16L) {
-                        @NotNull
+                    new Renderer.CanvasRenderer(
+                            surfaceHolder, userStyleRepository, watchState, CanvasType.SOFTWARE,
+                            16L) {
                         @Override
-                        public Bitmap takeScreenshot$wear_watchface_debug(
-                                @NotNull Calendar calendar,
-                                @NonNull RenderParameters renderParameters) {
-                            return null;
-                        }
+                        public void render(@NonNull Canvas canvas, @NonNull Rect bounds,
+                                @NonNull Calendar calendar) {
 
-                        @Override
-                        public void renderInternal$wear_watchface_debug(
-                                @NotNull Calendar calendar) {
                         }
                     }
             );
diff --git a/wear/wear-watchface-data/api/restricted_current.txt b/wear/wear-watchface-data/api/restricted_current.txt
index 8d43ba1..62ee931 100644
--- a/wear/wear-watchface-data/api/restricted_current.txt
+++ b/wear/wear-watchface-data/api/restricted_current.txt
@@ -188,10 +188,11 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public final class ComplicationStateWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public ComplicationStateWireFormat(android.graphics.Rect, @androidx.wear.watchface.data.ComplicationBoundsType int, @android.support.wearable.complications.ComplicationData.ComplicationType int[], java.util.List<android.content.ComponentName!>?, int, @android.support.wearable.complications.ComplicationData.ComplicationType int, boolean);
+    ctor public ComplicationStateWireFormat(android.graphics.Rect, @androidx.wear.watchface.data.ComplicationBoundsType int, @android.support.wearable.complications.ComplicationData.ComplicationType int[], java.util.List<android.content.ComponentName!>?, int, @android.support.wearable.complications.ComplicationData.ComplicationType int, boolean, @android.support.wearable.complications.ComplicationData.ComplicationType int);
     method public int describeContents();
     method public android.graphics.Rect getBounds();
     method @androidx.wear.watchface.data.ComplicationBoundsType public int getBoundsType();
+    method @android.support.wearable.complications.ComplicationData.ComplicationType public int getCurrentType();
     method @android.support.wearable.complications.ComplicationData.ComplicationType public int getDefaultProviderType();
     method public java.util.List<android.content.ComponentName!>? getDefaultProvidersToTry();
     method public int getFallbackSystemProvider();
@@ -231,9 +232,10 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class RenderParametersWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public RenderParametersWireFormat(int, java.util.List<androidx.wear.watchface.data.RenderParametersWireFormat.LayerParameterWireFormat!>, Integer?);
+    ctor public RenderParametersWireFormat(int, java.util.List<androidx.wear.watchface.data.RenderParametersWireFormat.LayerParameterWireFormat!>, Integer?, @ColorInt int);
     method public int describeContents();
     method public int getDrawMode();
+    method @ColorInt public int getHighlightTint();
     method public Integer? getHighlightedComplicationId();
     method public java.util.List<androidx.wear.watchface.data.RenderParametersWireFormat.LayerParameterWireFormat!> getLayerParameters();
     method public void writeToParcel(android.os.Parcel, int);
@@ -276,7 +278,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public static class ComplicationsUserStyleSettingWireFormat.ComplicationOverlayWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public ComplicationsUserStyleSettingWireFormat.ComplicationOverlayWireFormat(int, Boolean?, android.graphics.RectF?, int[]?, java.util.List<android.content.ComponentName!>?, Integer?, @android.support.wearable.complications.ComplicationData.ComplicationType Integer?);
+    ctor public ComplicationsUserStyleSettingWireFormat.ComplicationOverlayWireFormat(int, Boolean?, java.util.Map<androidx.wear.complications.data.ComplicationType!,android.graphics.RectF!>?, int[]?, java.util.List<android.content.ComponentName!>?, Integer?, @android.support.wearable.complications.ComplicationData.ComplicationType Integer?);
     method public int describeContents();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<androidx.wear.watchface.style.data.ComplicationsUserStyleSettingWireFormat.ComplicationOverlayWireFormat!>! CREATOR;
@@ -284,11 +286,11 @@
     field public static final int ENABLED_UNKNOWN = -1; // 0xffffffff
     field public static final int ENABLED_YES = 1; // 0x1
     field public static final int NO_DEFAULT_PROVIDER_TYPE = -1; // 0xffffffff
-    field @androidx.versionedparcelable.ParcelField(3) public android.graphics.RectF? mBounds;
     field @androidx.versionedparcelable.ParcelField(1) public int mComplicationId;
     field @android.support.wearable.complications.ComplicationData.ComplicationType @androidx.versionedparcelable.ParcelField(7) public int mDefaultProviderType;
     field @androidx.versionedparcelable.ParcelField(5) public java.util.List<android.content.ComponentName!>? mDefaultProviders;
     field @androidx.versionedparcelable.ParcelField(2) public int mEnabled;
+    field @androidx.versionedparcelable.ParcelField(3) public java.util.Map<androidx.wear.complications.data.ComplicationType!,android.graphics.RectF!>? mPerComplicationTypeBounds;
     field @androidx.versionedparcelable.ParcelField(4) public int[]? mSupportedTypes;
     field @androidx.versionedparcelable.ParcelField(6) @androidx.wear.complications.SystemProviders.ProviderId public int mSystemProviderFallback;
   }
diff --git a/wear/wear-watchface-data/build.gradle b/wear/wear-watchface-data/build.gradle
index f32aa2f..97a32fd 100644
--- a/wear/wear-watchface-data/build.gradle
+++ b/wear/wear-watchface-data/build.gradle
@@ -48,6 +48,10 @@
     buildFeatures {
         aidl = true
     }
+
+    buildTypes.all {
+        consumerProguardFiles 'proguard-rules.pro'
+    }
 }
 
 androidx {
diff --git a/wear/wear-watchface-data/proguard-rules.pro b/wear/wear-watchface-data/proguard-rules.pro
new file mode 100644
index 0000000..f50b7ac
--- /dev/null
+++ b/wear/wear-watchface-data/proguard-rules.pro
@@ -0,0 +1,16 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Prevent Parcelizer objects from being removed or renamed.
+-keep public class androidx.wear.watchface.**Parcelizer { *; }
\ No newline at end of file
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
index 40eb511..667c27f 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
@@ -32,7 +32,7 @@
 interface IInteractiveWatchFaceWCS {
     // IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
     // in the future to remain binary backwards compatible.
-    // Next Id: 12
+    // Next Id: 13
 
     /**
      * API version number. This should be incremented every time a new method is added.
@@ -114,4 +114,12 @@
      * @since API version 1.
      */
     oneway void release() = 11;
+
+    /**
+     * Requests the specified complication is highlighted for a short period to bring attention to
+     * it.
+     *
+     * @since API version 1.
+     */
+    oneway void bringAttentionToComplication(in int complicationId) = 12;
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java
index 9d5b032..6fb369ab 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java
@@ -69,6 +69,10 @@
     @ParcelField(7)
     boolean mIsEnabled;
 
+    @ParcelField(8)
+    @ComplicationData.ComplicationType
+    int mCurrentType;
+
     /** Used by VersionedParcelable. */
     ComplicationStateWireFormat() {
     }
@@ -80,7 +84,8 @@
             @Nullable List<ComponentName> defaultProvidersToTry,
             int fallbackSystemProvider,
             @ComplicationData.ComplicationType int defaultProviderType,
-            boolean isEnabled) {
+            boolean isEnabled,
+            @ComplicationData.ComplicationType int currentType) {
         mBounds = bounds;
         mBoundsType = boundsType;
         mSupportedTypes = supportedTypes;
@@ -88,6 +93,7 @@
         mFallbackSystemProvider = fallbackSystemProvider;
         mDefaultProviderType = defaultProviderType;
         mIsEnabled = isEnabled;
+        mCurrentType = currentType;
     }
 
     @NonNull
@@ -132,6 +138,12 @@
         return mIsEnabled;
     }
 
+    @NonNull
+    @ComplicationData.ComplicationType
+    public int getCurrentType() {
+        return mCurrentType;
+    }
+
     /** Serializes this ComplicationDetails to the specified {@link Parcel}. */
     @Override
     public void writeToParcel(@NonNull Parcel parcel, int flags) {
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java
index 4fec0b0..01a506d 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java
@@ -20,6 +20,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -39,7 +40,6 @@
 @VersionedParcelize
 @SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
 public class RenderParametersWireFormat implements VersionedParcelable, Parcelable {
-    /** */
     private static final int NO_COMPLICATION_ID = -1;
 
     /** Wire format for {@link androidx.wear.watchface.DrawMode}. */
@@ -55,6 +55,12 @@
     int mHighlightedComplicationId;
 
     /**
+     * Specifies the tint for any highlight.
+     */
+    @ParcelField(3)
+    int mHighlightTint;
+
+    /**
      * Wire format for Map<{@link androidx.wear.watchface.style.Layer},
      * {@link androidx.wear.watchface.LayerMode}>.
      *
@@ -66,18 +72,19 @@
     @ParcelField(100)
     List<LayerParameterWireFormat> mLayerParameters;
 
-
     RenderParametersWireFormat() {
     }
 
     public RenderParametersWireFormat(
             int drawMode,
             @NonNull List<LayerParameterWireFormat> layerParameters,
-            @Nullable Integer highlightedComplicationId) {
+            @Nullable Integer highlightedComplicationId,
+            @ColorInt int highlightTint) {
         mDrawMode = drawMode;
         mLayerParameters = layerParameters;
         mHighlightedComplicationId = (highlightedComplicationId != null)
                 ? highlightedComplicationId : NO_COMPLICATION_ID;
+        mHighlightTint = highlightTint;
     }
 
     public int getDrawMode() {
@@ -90,6 +97,11 @@
                 mHighlightedComplicationId;
     }
 
+    @ColorInt
+    public int getHighlightTint() {
+        return mHighlightTint;
+    }
+
     @NonNull
     public List<LayerParameterWireFormat> getLayerParameters() {
         return mLayerParameters;
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
index 2340a93..357f31a 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
@@ -32,8 +32,10 @@
 import androidx.versionedparcelable.VersionedParcelable;
 import androidx.versionedparcelable.VersionedParcelize;
 import androidx.wear.complications.SystemProviders;
+import androidx.wear.complications.data.ComplicationType;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * Wire format for {@link androidx.wear.watchface.style.ComplicationsUserStyleSetting}.
@@ -80,7 +82,7 @@
 
         @ParcelField(3)
         @Nullable
-        public RectF mBounds;
+        public Map<ComplicationType, RectF> mPerComplicationTypeBounds;
 
         @ParcelField(4)
         @Nullable
@@ -111,7 +113,7 @@
         public ComplicationOverlayWireFormat(
                 int complicationId,
                 @Nullable Boolean enabled,
-                @Nullable RectF bounds,
+                @Nullable Map<ComplicationType, RectF> perComplicationTypeBounds,
                 @Nullable int[] supportedTypes,
                 @Nullable List<ComponentName> defaultProviders,
                 @Nullable Integer systemProviderFallback,
@@ -123,7 +125,7 @@
             } else {
                 mEnabled = ENABLED_UNKNOWN;
             }
-            mBounds = bounds;
+            mPerComplicationTypeBounds = perComplicationTypeBounds;
             mSupportedTypes = supportedTypes;
             mDefaultProviders = defaultProviders;
             if (systemProviderFallback != null) {
diff --git a/wear/wear-watchface-style/api/current.txt b/wear/wear-watchface-style/api/current.txt
index 9b09a99..a049435 100644
--- a/wear/wear-watchface-style/api/current.txt
+++ b/wear/wear-watchface-style/api/current.txt
@@ -71,14 +71,14 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay(int complicationId, Boolean? enabled, android.graphics.RectF? bounds, java.util.List<? extends androidx.wear.complications.data.ComplicationType>? supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy, androidx.wear.complications.data.ComplicationType? defaultProviderType);
-    method public android.graphics.RectF? getBounds();
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay(int complicationId, Boolean? enabled, androidx.wear.complications.ComplicationBounds? complicationBounds, java.util.List<? extends androidx.wear.complications.data.ComplicationType>? supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy, androidx.wear.complications.data.ComplicationType? defaultProviderType);
+    method public androidx.wear.complications.ComplicationBounds? getComplicationBounds();
     method public int getComplicationId();
     method public androidx.wear.complications.DefaultComplicationProviderPolicy? getDefaultProviderPolicy();
     method public androidx.wear.complications.data.ComplicationType? getDefaultProviderType();
     method public java.util.List<androidx.wear.complications.data.ComplicationType>? getSupportedTypes();
     method public Boolean? isEnabled();
-    property public final android.graphics.RectF? bounds;
+    property public final androidx.wear.complications.ComplicationBounds? complicationBounds;
     property public final int complicationId;
     property public final androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy;
     property public final androidx.wear.complications.data.ComplicationType? defaultProviderType;
@@ -89,7 +89,7 @@
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder {
     ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder(int complicationId);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay build();
-    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setBounds(android.graphics.RectF bounds);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setComplicationBounds(androidx.wear.complications.ComplicationBounds complicationBounds);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setDefaultProviderPolicy(androidx.wear.complications.DefaultComplicationProviderPolicy? defaultComplicationProviderPolicy);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultComplicationProviderType);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setEnabled(boolean enabled);
diff --git a/wear/wear-watchface-style/api/public_plus_experimental_current.txt b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
index 622fce2..b3f4470 100644
--- a/wear/wear-watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
@@ -74,14 +74,14 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay(int complicationId, Boolean? enabled, android.graphics.RectF? bounds, java.util.List<? extends androidx.wear.complications.data.ComplicationType>? supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy, androidx.wear.complications.data.ComplicationType? defaultProviderType);
-    method public android.graphics.RectF? getBounds();
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay(int complicationId, Boolean? enabled, androidx.wear.complications.ComplicationBounds? complicationBounds, java.util.List<? extends androidx.wear.complications.data.ComplicationType>? supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy, androidx.wear.complications.data.ComplicationType? defaultProviderType);
+    method public androidx.wear.complications.ComplicationBounds? getComplicationBounds();
     method public int getComplicationId();
     method public androidx.wear.complications.DefaultComplicationProviderPolicy? getDefaultProviderPolicy();
     method public androidx.wear.complications.data.ComplicationType? getDefaultProviderType();
     method public java.util.List<androidx.wear.complications.data.ComplicationType>? getSupportedTypes();
     method public Boolean? isEnabled();
-    property public final android.graphics.RectF? bounds;
+    property public final androidx.wear.complications.ComplicationBounds? complicationBounds;
     property public final int complicationId;
     property public final androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy;
     property public final androidx.wear.complications.data.ComplicationType? defaultProviderType;
@@ -92,7 +92,7 @@
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder {
     ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder(int complicationId);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay build();
-    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setBounds(android.graphics.RectF bounds);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setComplicationBounds(androidx.wear.complications.ComplicationBounds complicationBounds);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setDefaultProviderPolicy(androidx.wear.complications.DefaultComplicationProviderPolicy? defaultComplicationProviderPolicy);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultComplicationProviderType);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setEnabled(boolean enabled);
diff --git a/wear/wear-watchface-style/api/restricted_current.txt b/wear/wear-watchface-style/api/restricted_current.txt
index b05e004..162e2b9 100644
--- a/wear/wear-watchface-style/api/restricted_current.txt
+++ b/wear/wear-watchface-style/api/restricted_current.txt
@@ -80,14 +80,14 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay(int complicationId, Boolean? enabled, android.graphics.RectF? bounds, java.util.List<? extends androidx.wear.complications.data.ComplicationType>? supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy, androidx.wear.complications.data.ComplicationType? defaultProviderType);
-    method public android.graphics.RectF? getBounds();
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay(int complicationId, Boolean? enabled, androidx.wear.complications.ComplicationBounds? complicationBounds, java.util.List<? extends androidx.wear.complications.data.ComplicationType>? supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy, androidx.wear.complications.data.ComplicationType? defaultProviderType);
+    method public androidx.wear.complications.ComplicationBounds? getComplicationBounds();
     method public int getComplicationId();
     method public androidx.wear.complications.DefaultComplicationProviderPolicy? getDefaultProviderPolicy();
     method public androidx.wear.complications.data.ComplicationType? getDefaultProviderType();
     method public java.util.List<androidx.wear.complications.data.ComplicationType>? getSupportedTypes();
     method public Boolean? isEnabled();
-    property public final android.graphics.RectF? bounds;
+    property public final androidx.wear.complications.ComplicationBounds? complicationBounds;
     property public final int complicationId;
     property public final androidx.wear.complications.DefaultComplicationProviderPolicy? defaultProviderPolicy;
     property public final androidx.wear.complications.data.ComplicationType? defaultProviderType;
@@ -98,7 +98,7 @@
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder {
     ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder(int complicationId);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay build();
-    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setBounds(android.graphics.RectF bounds);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setComplicationBounds(androidx.wear.complications.ComplicationBounds complicationBounds);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setDefaultProviderPolicy(androidx.wear.complications.DefaultComplicationProviderPolicy? defaultComplicationProviderPolicy);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultComplicationProviderType);
     method public androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay.Builder setEnabled(boolean enabled);
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
index 1161815..ad56258 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
@@ -88,8 +88,9 @@
 }
 
 /**
- * In memory storage for user style choices which allows listeners to be registered to observe
- * style changes.
+ * An in memory storage for user style choices represented as [UserStyle], listeners can be
+ * registered to observe style changes. The UserStyleRepository is initialized with a
+ * [UserStyleSchema].
  */
 public class UserStyleRepository(
     /**
@@ -98,37 +99,36 @@
      */
     public val schema: UserStyleSchema
 ) {
-    /** A listener for observing user style changes. */
+    /** A listener for observing [UserStyle] changes. */
     public interface UserStyleListener {
-        /** Called whenever the user style changes. */
+        /** Called whenever the [UserStyle] changes. */
         @UiThread
         public fun onUserStyleChanged(userStyle: UserStyle)
     }
 
     private val styleListeners = HashSet<UserStyleListener>()
 
-    // The current style state which is initialized from the userStyleSettings.
-    @SuppressWarnings("SyntheticAccessor")
-    private val _style = UserStyle(
+    /**
+     * The current [UserStyle]. Assigning to this property triggers immediate [UserStyleListener]
+     * callbacks if if any options have changed.
+     */
+    public var userStyle: UserStyle = UserStyle(
         HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
             for (setting in schema.userStyleSettings) {
                 this[setting] = setting.getDefaultOption()
             }
         }
     )
-
-    /** The current user controlled style for rendering etc... */
-    public var userStyle: UserStyle
         @UiThread
-        get() = _style
+        get
         @UiThread
         set(style) {
             var changed = false
             val hashmap =
-                _style.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
+                field.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
             for ((setting, option) in style.selectedOptions) {
                 // Ignore an unrecognized setting.
-                val styleSetting = _style.selectedOptions[setting] ?: continue
+                val styleSetting = field.selectedOptions[setting] ?: continue
                 if (styleSetting.id != option.id) {
                     changed = true
                 }
@@ -140,7 +140,7 @@
             }
 
             for (styleListener in styleListeners) {
-                styleListener.onUserStyleChanged(_style)
+                styleListener.onUserStyleChanged(field)
             }
         }
 
@@ -151,7 +151,7 @@
     @SuppressLint("ExecutorRegistration")
     public fun addUserStyleListener(userStyleListener: UserStyleListener) {
         styleListeners.add(userStyleListener)
-        userStyleListener.onUserStyleChanged(_style)
+        userStyleListener.onUserStyleChanged(userStyle)
     }
 
     /** Removes a [UserStyleListener] previously added by [addUserStyleListener]. */
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 978e023..8903a6d 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -16,9 +16,9 @@
 
 package androidx.wear.watchface.style
 
-import android.graphics.RectF
 import android.graphics.drawable.Icon
 import androidx.annotation.RestrictTo
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
@@ -32,10 +32,12 @@
 import java.security.InvalidParameterException
 
 /**
- * Watch faces often have user configurable styles. The definition of what is a style is left up
- * to the watch face but it typically incorporates a variety of settings such as: color,
- * visual theme for watch hands, font, tick shape, complications, audio elements, etc...
- * A UserStyleSetting represents one of these dimensions.
+ * Watch faces often have user configurable styles, the definition of what is a style is left up to
+ * the watch face but it typically incorporates a variety of settings such as: color, visual theme
+ * for watch hands, font, tick shape, complications, audio elements, etc...
+ *
+ * A UserStyleSetting represents one of these dimensions. See also [UserStyleSchema] which defines
+ * the list of UserStyleSettings provided by the watch face.
  */
 public sealed class UserStyleSetting(
     /** Identifier for the element, must be unique. */
@@ -291,10 +293,10 @@
             public val enabled: Boolean? = null,
 
             /**
-             * If non null, the new unit square screen space complication bounds for this
-             * configuration. If null then no changes are made.
+             * If non null, the new [ComplicationBounds] for this configuration. If null then no
+             * changes are made.
              */
-            public val bounds: RectF? = null,
+            public val complicationBounds: ComplicationBounds? = null,
 
             /**
              * If non null, the new types of complication supported by this complication for this
@@ -319,7 +321,7 @@
                 private val complicationId: Int
             ) {
                 private var enabled: Boolean? = null
-                private var bounds: RectF? = null
+                private var complicationBounds: ComplicationBounds? = null
                 private var supportedTypes: List<ComplicationType>? = null
                 private var defaultComplicationProviderPolicy: DefaultComplicationProviderPolicy? =
                     null
@@ -330,10 +332,11 @@
                     this.enabled = enabled
                 }
 
-                /** Overrides the complication's unit-square screen space bounds. */
-                public fun setBounds(bounds: RectF): Builder = apply {
-                    this.bounds = bounds
-                }
+                /** Overrides the complication's per [ComplicationBounds]. */
+                public fun setComplicationBounds(complicationBounds: ComplicationBounds): Builder =
+                    apply {
+                        this.complicationBounds = complicationBounds
+                    }
 
                 /** Overrides the complication's supported complication types. */
                 public fun setSupportedTypes(supportedTypes: List<ComplicationType>): Builder =
@@ -357,14 +360,15 @@
                     this.defaultComplicationProviderType = defaultComplicationProviderType
                 }
 
-                public fun build(): ComplicationOverlay = ComplicationOverlay(
-                    complicationId,
-                    enabled,
-                    bounds,
-                    supportedTypes,
-                    defaultComplicationProviderPolicy,
-                    defaultComplicationProviderType
-                )
+                public fun build(): ComplicationOverlay =
+                    ComplicationOverlay(
+                        complicationId,
+                        enabled,
+                        complicationBounds,
+                        supportedTypes,
+                        defaultComplicationProviderPolicy,
+                        defaultComplicationProviderType
+                    )
             }
 
             internal constructor(
@@ -382,7 +386,7 @@
                         "Unrecognised wireFormat.mEnabled " + wireFormat.mEnabled
                     )
                 },
-                wireFormat.mBounds,
+                wireFormat.mPerComplicationTypeBounds?.let { ComplicationBounds(it) },
                 wireFormat.mSupportedTypes?.let { ComplicationType.fromWireTypeList(it) },
                 wireFormat.mDefaultProviders?.let {
                     DefaultComplicationProviderPolicy(it, wireFormat.mSystemProviderFallback)
@@ -401,7 +405,7 @@
                 ComplicationsUserStyleSettingWireFormat.ComplicationOverlayWireFormat(
                     complicationId,
                     enabled,
-                    bounds,
+                    complicationBounds?.perComplicationTypeBounds,
                     supportedTypes?.let { ComplicationType.toWireTypes(it) },
                     defaultProviderPolicy?.providersAsList(),
                     defaultProviderPolicy?.systemProviderFallback,
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index dd7e387..4fb343b 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -15,7 +15,7 @@
 
   public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
     ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
-    method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+    method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, @ColorInt int color);
     method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
     method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
     method @UiThread public boolean isHighlighted();
@@ -30,37 +30,28 @@
     property @UiThread public boolean isHighlighted;
   }
 
-  public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
-  }
-
   public final class Complication {
     method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
-    method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
+    method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
+    method @UiThread public androidx.wear.complications.ComplicationBounds getComplicationBounds();
     method public androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> getComplicationData();
     method @UiThread public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
     method @UiThread public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
     method @UiThread public androidx.wear.watchface.CanvasComplication getRenderer();
     method @UiThread public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
-    method @UiThread public android.graphics.RectF getUnitSquareBounds();
     method public void invalidate();
     method public boolean isActiveAt(long dateTimeMillis);
     method @UiThread public boolean isEnabled();
     method @UiThread public void render(android.graphics.Canvas canvas, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
-    method @UiThread public void setDefaultProviderPolicy(androidx.wear.complications.DefaultComplicationProviderPolicy value);
-    method @UiThread public void setDefaultProviderType(androidx.wear.complications.data.ComplicationType value);
-    method @UiThread public void setEnabled(boolean value);
+    method @UiThread public void setComplicationBounds(androidx.wear.complications.ComplicationBounds value);
     method @UiThread public void setRenderer(androidx.wear.watchface.CanvasComplication value);
-    method @UiThread public void setSupportedTypes(java.util.List<? extends androidx.wear.complications.data.ComplicationType> value);
-    method @UiThread public void setUnitSquareBounds(android.graphics.RectF value);
+    property @UiThread public final androidx.wear.complications.ComplicationBounds complicationBounds;
     property public final androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> complicationData;
     property @UiThread public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
     property @UiThread public final androidx.wear.complications.data.ComplicationType defaultProviderType;
     property @UiThread public final boolean enabled;
     property @UiThread public final androidx.wear.watchface.CanvasComplication renderer;
     property @UiThread public final java.util.List<androidx.wear.complications.data.ComplicationType> supportedTypes;
-    property @UiThread public final android.graphics.RectF unitSquareBounds;
     field public static final androidx.wear.watchface.Complication.Companion Companion;
   }
 
@@ -71,17 +62,17 @@
 
   public static final class Complication.Companion {
     method public androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
-    method public androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
+    method public androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
   }
 
   public final class ComplicationOutlineRenderer {
     ctor public ComplicationOutlineRenderer();
-    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
     field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
   }
 
   public static final class ComplicationOutlineRenderer.Companion {
-    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
   }
 
   public final class ComplicationsManager {
@@ -111,18 +102,6 @@
     enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
   }
 
-  public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public void onGlContextCreated();
-    method @UiThread public void onGlSurfaceCreated(int width, int height);
-    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
-  }
-
-  public final class GlesRendererKt {
-  }
-
   public final class GlesTextureComplication {
     ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
     method public void bind();
@@ -147,8 +126,8 @@
   public class ObservableWatchData<T> {
     method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread public T getValue();
-    method public final T getValueOr(T p);
-    method public final boolean hasValue();
+    method @UiThread public final T getValueOr(T p);
+    method @UiThread public final boolean hasValue();
     method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread protected void setValue(T v);
     property @UiThread public T value;
@@ -159,11 +138,13 @@
   }
 
   public final class RenderParameters {
-    ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId);
+    ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId, @ColorInt int highlightTint);
     method public androidx.wear.watchface.DrawMode getDrawMode();
+    method public int getHighlightTint();
     method public Integer? getHighlightedComplicationId();
     method public java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> getLayerParameters();
     property public final androidx.wear.watchface.DrawMode drawMode;
+    property public final int highlightTint;
     property public final Integer? highlightedComplicationId;
     property public final java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> layerParameters;
     field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
@@ -174,8 +155,7 @@
   public static final class RenderParameters.Companion {
   }
 
-  public abstract class Renderer {
-    ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+  public abstract sealed class Renderer {
     method public final float getCenterX();
     method public final float getCenterY();
     method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -197,6 +177,23 @@
     property public final android.view.SurfaceHolder surfaceHolder;
   }
 
+  public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+  }
+
+  public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public void onGlContextCreated();
+    method @UiThread public void onGlSurfaceCreated(int width, int height);
+    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+  }
+
+  public final class RendererKt {
+  }
+
   public final class WatchFace {
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -234,7 +231,7 @@
   public final class WatchFaceKt {
   }
 
-  @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+  public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
     ctor public WatchFaceService();
     method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
     method public final android.service.wallpaper.WallpaperService.Engine onCreateEngine();
@@ -244,11 +241,10 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -260,7 +256,6 @@
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
 }
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index dd7e387..4fb343b 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -15,7 +15,7 @@
 
   public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
     ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
-    method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+    method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, @ColorInt int color);
     method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
     method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
     method @UiThread public boolean isHighlighted();
@@ -30,37 +30,28 @@
     property @UiThread public boolean isHighlighted;
   }
 
-  public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
-  }
-
   public final class Complication {
     method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
-    method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
+    method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
+    method @UiThread public androidx.wear.complications.ComplicationBounds getComplicationBounds();
     method public androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> getComplicationData();
     method @UiThread public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
     method @UiThread public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
     method @UiThread public androidx.wear.watchface.CanvasComplication getRenderer();
     method @UiThread public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
-    method @UiThread public android.graphics.RectF getUnitSquareBounds();
     method public void invalidate();
     method public boolean isActiveAt(long dateTimeMillis);
     method @UiThread public boolean isEnabled();
     method @UiThread public void render(android.graphics.Canvas canvas, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
-    method @UiThread public void setDefaultProviderPolicy(androidx.wear.complications.DefaultComplicationProviderPolicy value);
-    method @UiThread public void setDefaultProviderType(androidx.wear.complications.data.ComplicationType value);
-    method @UiThread public void setEnabled(boolean value);
+    method @UiThread public void setComplicationBounds(androidx.wear.complications.ComplicationBounds value);
     method @UiThread public void setRenderer(androidx.wear.watchface.CanvasComplication value);
-    method @UiThread public void setSupportedTypes(java.util.List<? extends androidx.wear.complications.data.ComplicationType> value);
-    method @UiThread public void setUnitSquareBounds(android.graphics.RectF value);
+    property @UiThread public final androidx.wear.complications.ComplicationBounds complicationBounds;
     property public final androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> complicationData;
     property @UiThread public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
     property @UiThread public final androidx.wear.complications.data.ComplicationType defaultProviderType;
     property @UiThread public final boolean enabled;
     property @UiThread public final androidx.wear.watchface.CanvasComplication renderer;
     property @UiThread public final java.util.List<androidx.wear.complications.data.ComplicationType> supportedTypes;
-    property @UiThread public final android.graphics.RectF unitSquareBounds;
     field public static final androidx.wear.watchface.Complication.Companion Companion;
   }
 
@@ -71,17 +62,17 @@
 
   public static final class Complication.Companion {
     method public androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
-    method public androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
+    method public androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
   }
 
   public final class ComplicationOutlineRenderer {
     ctor public ComplicationOutlineRenderer();
-    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
     field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
   }
 
   public static final class ComplicationOutlineRenderer.Companion {
-    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
   }
 
   public final class ComplicationsManager {
@@ -111,18 +102,6 @@
     enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
   }
 
-  public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public void onGlContextCreated();
-    method @UiThread public void onGlSurfaceCreated(int width, int height);
-    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
-  }
-
-  public final class GlesRendererKt {
-  }
-
   public final class GlesTextureComplication {
     ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
     method public void bind();
@@ -147,8 +126,8 @@
   public class ObservableWatchData<T> {
     method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread public T getValue();
-    method public final T getValueOr(T p);
-    method public final boolean hasValue();
+    method @UiThread public final T getValueOr(T p);
+    method @UiThread public final boolean hasValue();
     method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread protected void setValue(T v);
     property @UiThread public T value;
@@ -159,11 +138,13 @@
   }
 
   public final class RenderParameters {
-    ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId);
+    ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId, @ColorInt int highlightTint);
     method public androidx.wear.watchface.DrawMode getDrawMode();
+    method public int getHighlightTint();
     method public Integer? getHighlightedComplicationId();
     method public java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> getLayerParameters();
     property public final androidx.wear.watchface.DrawMode drawMode;
+    property public final int highlightTint;
     property public final Integer? highlightedComplicationId;
     property public final java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> layerParameters;
     field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
@@ -174,8 +155,7 @@
   public static final class RenderParameters.Companion {
   }
 
-  public abstract class Renderer {
-    ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+  public abstract sealed class Renderer {
     method public final float getCenterX();
     method public final float getCenterY();
     method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -197,6 +177,23 @@
     property public final android.view.SurfaceHolder surfaceHolder;
   }
 
+  public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+  }
+
+  public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public void onGlContextCreated();
+    method @UiThread public void onGlSurfaceCreated(int width, int height);
+    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+  }
+
+  public final class RendererKt {
+  }
+
   public final class WatchFace {
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -234,7 +231,7 @@
   public final class WatchFaceKt {
   }
 
-  @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+  public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
     ctor public WatchFaceService();
     method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
     method public final android.service.wallpaper.WallpaperService.Engine onCreateEngine();
@@ -244,11 +241,10 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -260,7 +256,6 @@
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
 }
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index c6773f25..3ea9707 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -15,7 +15,7 @@
 
   public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
     ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
-    method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+    method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, @ColorInt int color);
     method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
     method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
     method @UiThread public boolean isHighlighted();
@@ -30,37 +30,28 @@
     property @UiThread public boolean isHighlighted;
   }
 
-  public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
-  }
-
   public final class Complication {
     method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
-    method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
+    method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
+    method @UiThread public androidx.wear.complications.ComplicationBounds getComplicationBounds();
     method public androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> getComplicationData();
     method @UiThread public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
     method @UiThread public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
     method @UiThread public androidx.wear.watchface.CanvasComplication getRenderer();
     method @UiThread public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
-    method @UiThread public android.graphics.RectF getUnitSquareBounds();
     method public void invalidate();
     method public boolean isActiveAt(long dateTimeMillis);
     method @UiThread public boolean isEnabled();
     method @UiThread public void render(android.graphics.Canvas canvas, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
-    method @UiThread public void setDefaultProviderPolicy(androidx.wear.complications.DefaultComplicationProviderPolicy value);
-    method @UiThread public void setDefaultProviderType(androidx.wear.complications.data.ComplicationType value);
-    method @UiThread public void setEnabled(boolean value);
+    method @UiThread public void setComplicationBounds(androidx.wear.complications.ComplicationBounds value);
     method @UiThread public void setRenderer(androidx.wear.watchface.CanvasComplication value);
-    method @UiThread public void setSupportedTypes(java.util.List<? extends androidx.wear.complications.data.ComplicationType> value);
-    method @UiThread public void setUnitSquareBounds(android.graphics.RectF value);
+    property @UiThread public final androidx.wear.complications.ComplicationBounds complicationBounds;
     property public final androidx.wear.watchface.ObservableWatchData<androidx.wear.complications.data.ComplicationData> complicationData;
     property @UiThread public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
     property @UiThread public final androidx.wear.complications.data.ComplicationType defaultProviderType;
     property @UiThread public final boolean enabled;
     property @UiThread public final androidx.wear.watchface.CanvasComplication renderer;
     property @UiThread public final java.util.List<androidx.wear.complications.data.ComplicationType> supportedTypes;
-    property @UiThread public final android.graphics.RectF unitSquareBounds;
     field public static final androidx.wear.watchface.Complication.Companion Companion;
   }
 
@@ -71,17 +62,17 @@
 
   public static final class Complication.Companion {
     method public androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
-    method public androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
+    method public androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.ComplicationBounds complicationBounds);
   }
 
   public final class ComplicationOutlineRenderer {
     ctor public ComplicationOutlineRenderer();
-    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+    method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
     field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
   }
 
   public static final class ComplicationOutlineRenderer.Companion {
-    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+    method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
   }
 
   public final class ComplicationsManager {
@@ -111,18 +102,6 @@
     enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
   }
 
-  public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public void onGlContextCreated();
-    method @UiThread public void onGlSurfaceCreated(int width, int height);
-    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
-  }
-
-  public final class GlesRendererKt {
-  }
-
   public final class GlesTextureComplication {
     ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
     method public void bind();
@@ -151,7 +130,6 @@
     method public boolean getHasBurnInProtection();
     method public boolean getHasLowBitAmbient();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible();
@@ -160,7 +138,6 @@
     method public void setHasBurnInProtection(boolean p);
     method public void setHasLowBitAmbient(boolean p);
     method public void setInterruptionFilter(androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> p);
-    method public void setScreenShape(int p);
     property public final long analogPreviewReferenceTimeMillis;
     property public final long digitalPreviewReferenceTimeMillis;
     property public final boolean hasBurnInProtection;
@@ -169,14 +146,13 @@
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging;
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
   public class ObservableWatchData<T> {
     method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread public T getValue();
-    method public final T getValueOr(T p);
-    method public final boolean hasValue();
+    method @UiThread public final T getValueOr(T p);
+    method @UiThread public final boolean hasValue();
     method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread protected void setValue(T v);
     property @UiThread public T value;
@@ -187,13 +163,15 @@
   }
 
   public final class RenderParameters {
-    ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId);
+    ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId, @ColorInt int highlightTint);
     ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public RenderParameters(androidx.wear.watchface.data.RenderParametersWireFormat wireFormat);
     method public androidx.wear.watchface.DrawMode getDrawMode();
+    method public int getHighlightTint();
     method public Integer? getHighlightedComplicationId();
     method public java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> getLayerParameters();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.data.RenderParametersWireFormat toWireFormat();
     property public final androidx.wear.watchface.DrawMode drawMode;
+    property public final int highlightTint;
     property public final Integer? highlightedComplicationId;
     property public final java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> layerParameters;
     field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
@@ -204,8 +182,7 @@
   public static final class RenderParameters.Companion {
   }
 
-  public abstract class Renderer {
-    ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+  public abstract sealed class Renderer {
     method public final float getCenterX();
     method public final float getCenterY();
     method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -227,6 +204,23 @@
     property public final android.view.SurfaceHolder surfaceHolder;
   }
 
+  public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+  }
+
+  public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public void onGlContextCreated();
+    method @UiThread public void onGlSurfaceCreated(int width, int height);
+    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+  }
+
+  public final class RendererKt {
+  }
+
   public final class WatchFace {
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -281,7 +275,7 @@
   public final class WatchFaceKt {
   }
 
-  @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+  public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
     ctor public WatchFaceService();
     method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public android.view.SurfaceHolder? getWallpaperSurfaceHolderOverride();
@@ -292,11 +286,10 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -308,7 +301,6 @@
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
 }
diff --git a/wear/wear-watchface/build.gradle b/wear/wear-watchface/build.gradle
index beca881..f4a1682 100644
--- a/wear/wear-watchface/build.gradle
+++ b/wear/wear-watchface/build.gradle
@@ -61,7 +61,7 @@
 
 android {
     defaultConfig {
-        minSdkVersion 26
+        minSdkVersion 25
         testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
     }
 
diff --git a/wear/wear-watchface/samples/build.gradle b/wear/wear-watchface/samples/build.gradle
index ec111e8..ec2f092 100644
--- a/wear/wear-watchface/samples/build.gradle
+++ b/wear/wear-watchface/samples/build.gradle
@@ -44,10 +44,10 @@
 
 android {
     defaultConfig {
-        minSdkVersion 26
+        minSdkVersion 25
     }
     compileOptions {
-        sourceCompatibility = JavaVersion.VERSION_1_7
-        targetCompatibility = JavaVersion.VERSION_1_7
+        sourceCompatibility 1.8
+        targetCompatibility 1.8
     }
 }
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index 19dee75..1324b1f 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -26,15 +26,16 @@
 import android.graphics.drawable.Icon
 import android.icu.util.Calendar
 import android.view.SurfaceHolder
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
-import androidx.wear.watchface.CanvasRenderer
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -68,6 +69,16 @@
 
 private const val NUMBER_RADIUS_FRACTION = 0.45f
 
+val COLOR_STYLE_SETTING = "color_style_setting"
+val RED_STYLE = "red_style"
+val GREEN_STYLE = "green_style"
+val BLUE_STYLE = "blue_style"
+
+val DRAW_HOUR_PIPS_STYLE_SETTING = "draw_hour_pips_style_setting"
+
+val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
+
+val COMPLICATIONS_STYLE_SETTING = "complications_style_setting"
 val NO_COMPLICATIONS = "NO_COMPLICATIONS"
 val LEFT_COMPLICATION = "LEFT_COMPLICATION"
 val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
@@ -96,43 +107,43 @@
     surfaceHolder: SurfaceHolder,
     watchState: WatchState
 ): WatchFace {
-    val watchFaceStyle = WatchFaceColorStyle.create(context, "red_style")
+    val watchFaceStyle = WatchFaceColorStyle.create(context, RED_STYLE)
     val colorStyleSetting = ListUserStyleSetting(
-        "color_style_setting",
-        "Colors",
-        "Watchface colorization",
+        COLOR_STYLE_SETTING,
+        context.getString(R.string.colors_style_setting),
+        context.getString(R.string.colors_style_setting_description),
         icon = null,
         options = listOf(
             ListUserStyleSetting.ListOption(
-                "red_style",
-                "Red",
+                RED_STYLE,
+                context.getString(R.string.colors_style_red),
                 Icon.createWithResource(context, R.drawable.red_style)
             ),
             ListUserStyleSetting.ListOption(
-                "green_style",
-                "Green",
+                GREEN_STYLE,
+                context.getString(R.string.colors_style_green),
                 Icon.createWithResource(context, R.drawable.green_style)
             ),
             ListUserStyleSetting.ListOption(
-                "blue_style",
-                "Blue",
+                BLUE_STYLE,
+                context.getString(R.string.colors_style_blue),
                 Icon.createWithResource(context, R.drawable.blue_style)
             )
         ),
         listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
     )
     val drawHourPipsStyleSetting = BooleanUserStyleSetting(
-        "draw_hour_pips_style_setting",
-        "Hour Pips",
-        "Whether to draw or not",
+        DRAW_HOUR_PIPS_STYLE_SETTING,
+        context.getString(R.string.watchface_pips_setting),
+        context.getString(R.string.watchface_pips_setting_description),
         null,
         true,
         listOf(Layer.BASE_LAYER)
     )
     val watchHandLengthStyleSetting = DoubleRangeUserStyleSetting(
-        "watch_hand_length_style_setting",
-        "Hand length",
-        "Scale of watch hands",
+        WATCH_HAND_LENGTH_STYLE_SETTING,
+        context.getString(R.string.watchface_hand_length_setting),
+        context.getString(R.string.watchface_hand_length_setting_description),
         null,
         0.25,
         1.0,
@@ -142,14 +153,14 @@
     // These are style overrides applied on top of the complications passed into
     // complicationsManager below.
     val complicationsStyleSetting = ComplicationsUserStyleSetting(
-        "complications_style_setting",
-        "Complications",
-        "Number and position",
+        COMPLICATIONS_STYLE_SETTING,
+        context.getString(R.string.watchface_complications_setting),
+        context.getString(R.string.watchface_complications_setting_description),
         icon = null,
         complicationConfig = listOf(
             ComplicationsUserStyleSetting.ComplicationsOption(
                 LEFT_AND_RIGHT_COMPLICATIONS,
-                "Both",
+                context.getString(R.string.watchface_complications_setting_both),
                 null,
                 // NB this list is empty because each [ComplicationOverlay] is applied on top of
                 // the initial config.
@@ -157,7 +168,7 @@
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
                 NO_COMPLICATIONS,
-                "None",
+                context.getString(R.string.watchface_complications_setting_none),
                 null,
                 listOf(
                     ComplicationOverlay(
@@ -172,7 +183,7 @@
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
                 LEFT_COMPLICATION,
-                "Left",
+                context.getString(R.string.watchface_complications_setting_left),
                 null,
                 listOf(
                     ComplicationOverlay(
@@ -183,7 +194,7 @@
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
                 RIGHT_COMPLICATION,
-                "Right",
+                context.getString(R.string.watchface_complications_setting_right),
                 null,
                 listOf(
                     ComplicationOverlay(
@@ -216,7 +227,7 @@
             ComplicationType.SMALL_IMAGE
         ),
         DefaultComplicationProviderPolicy(SystemProviders.DAY_OF_WEEK),
-        RectF(0.2f, 0.4f, 0.4f, 0.6f)
+        ComplicationBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
     ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
         .build()
     val rightComplication = Complication.createRoundRectComplicationBuilder(
@@ -230,7 +241,7 @@
             ComplicationType.SMALL_IMAGE
         ),
         DefaultComplicationProviderPolicy(SystemProviders.STEP_COUNT),
-        RectF(0.6f, 0.4f, 0.8f, 0.6f)
+        ComplicationBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
     ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
         .build()
     val complicationsManager = ComplicationsManager(
@@ -266,7 +277,7 @@
     private val drawPipsStyleSetting: BooleanUserStyleSetting,
     private val watchHandLengthStyleSettingDouble: DoubleRangeUserStyleSetting,
     private val complicationsManager: ComplicationsManager
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
     surfaceHolder,
     userStyleRepository,
     watchState,
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index 94cc130..cedac02 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -38,15 +38,17 @@
 import android.view.SurfaceHolder
 import android.view.animation.AnimationUtils
 import android.view.animation.PathInterpolator
+import androidx.annotation.ColorInt
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
-import androidx.wear.watchface.CanvasRenderer
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -193,6 +195,19 @@
 internal val CENTERING_ADJUSTMENT_INTERPOLATOR =
     PathInterpolator(0.4f, 0f, 0.2f, 1f)
 
+@ColorInt
+internal fun colorRgb(red: Float, green: Float, blue: Float) =
+    0xff000000.toInt() or
+        ((red * 255.0f + 0.5f).toInt() shl 16) or
+        ((green * 255.0f + 0.5f).toInt() shl 8) or
+        (blue * 255.0f + 0.5f).toInt()
+
+internal fun redFraction(@ColorInt color: Int) = Color.red(color).toFloat() / 255.0f
+
+internal fun greenFraction(@ColorInt color: Int) = Color.green(color).toFloat() / 255.0f
+
+internal fun blueFraction(@ColorInt color: Int) = Color.blue(color).toFloat() / 255.0f
+
 /**
  * Returns an RGB color that has the same effect as drawing `color` with `alphaFraction` over a
  * `backgroundColor` background.
@@ -201,11 +216,15 @@
  * @param alphaFraction the fraction of the alpha value, range from 0 to 1
  * @param backgroundColor the background color
  */
-internal fun getRGBColor(color: Color, alphaFraction: Float, backgroundColor: Color): Int {
-    return Color.rgb(
-        lerp(backgroundColor.red(), color.red(), alphaFraction),
-        lerp(backgroundColor.green(), color.green(), alphaFraction),
-        lerp(backgroundColor.blue(), color.blue(), alphaFraction)
+internal fun getRGBColor(
+    @ColorInt color: Int,
+    alphaFraction: Float,
+    @ColorInt backgroundColor: Int
+): Int {
+    return colorRgb(
+        lerp(redFraction(backgroundColor), redFraction(color), alphaFraction),
+        lerp(greenFraction(backgroundColor), greenFraction(color), alphaFraction),
+        lerp(blueFraction(backgroundColor), blueFraction(color), alphaFraction)
     )
 }
 
@@ -417,12 +436,11 @@
 
 /** Applies a multiplier to a color, e.g. to darken if it's < 1.0 */
 internal fun multiplyColor(colorInt: Int, multiplier: Float): Int {
-    val color = Color.valueOf(colorInt)
-    // NB color.red() etc return a value in the range [0..1]
-    return Color.rgb(
-        color.red() * multiplier,
-        color.green() * multiplier,
-        color.blue() * multiplier
+    val adjustedMultiplier = multiplier / 255.0f
+    return colorRgb(
+        Color.red(colorInt).toFloat() * adjustedMultiplier,
+        Color.green(colorInt).toFloat() * adjustedMultiplier,
+        Color.blue(colorInt).toFloat() * adjustedMultiplier,
     )
 }
 
@@ -450,26 +468,26 @@
         surfaceHolder: SurfaceHolder,
         watchState: WatchState
     ): WatchFace {
-        val watchFaceStyle = WatchFaceColorStyle.create(this, "red_style")
+        val watchFaceStyle = WatchFaceColorStyle.create(this, RED_STYLE)
         val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
-            "color_style_setting",
-            "Colors",
-            "Watchface colorization",
+            COLOR_STYLE_SETTING,
+            getString(R.string.colors_style_setting),
+            getString(R.string.colors_style_setting_description),
             icon = null,
             options = listOf(
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    "red_style",
-                    "Red",
+                    RED_STYLE,
+                    getString(R.string.colors_style_red),
                     Icon.createWithResource(this, R.drawable.red_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    "green_style",
-                    "Green",
+                    GREEN_STYLE,
+                    getString(R.string.colors_style_green),
                     Icon.createWithResource(this, R.drawable.green_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    "blue_style",
-                    "Blue",
+                    BLUE_STYLE,
+                    getString(R.string.colors_style_blue),
                     Icon.createWithResource(this, R.drawable.blue_style)
                 )
             ),
@@ -488,9 +506,11 @@
                 ComplicationType.SMALL_IMAGE
             ),
             DefaultComplicationProviderPolicy(SystemProviders.WATCH_BATTERY),
-            createBoundsRect(
-                LEFT_CIRCLE_COMPLICATION_CENTER_FRACTION,
-                CIRCLE_COMPLICATION_DIAMETER_FRACTION
+            ComplicationBounds(
+                createBoundsRect(
+                    LEFT_CIRCLE_COMPLICATION_CENTER_FRACTION,
+                    CIRCLE_COMPLICATION_DIAMETER_FRACTION
+                )
             )
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
@@ -504,43 +524,64 @@
                 ComplicationType.SMALL_IMAGE
             ),
             DefaultComplicationProviderPolicy(SystemProviders.DATE),
-            createBoundsRect(
-                RIGHT_CIRCLE_COMPLICATION_CENTER_FRACTION,
-                CIRCLE_COMPLICATION_DIAMETER_FRACTION
+            ComplicationBounds(
+                createBoundsRect(
+                    RIGHT_CIRCLE_COMPLICATION_CENTER_FRACTION,
+                    CIRCLE_COMPLICATION_DIAMETER_FRACTION
+                )
             )
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
+
+        val upperAndLowerComplicationTypes = listOf(
+            ComplicationType.LONG_TEXT,
+            ComplicationType.RANGED_VALUE,
+            ComplicationType.SHORT_TEXT,
+            ComplicationType.MONOCHROMATIC_IMAGE,
+            ComplicationType.SMALL_IMAGE
+        )
+        // The upper and lower complications change shape depending on the complication's type.
         val upperComplication = Complication.createRoundRectComplicationBuilder(
             ComplicationID.UPPER.ordinal,
             watchFaceStyle.getComplicationDrawableRenderer(this, watchState),
-            listOf(
-                ComplicationType.LONG_TEXT,
-                ComplicationType.RANGED_VALUE,
-                ComplicationType.SHORT_TEXT,
-                ComplicationType.MONOCHROMATIC_IMAGE,
-                ComplicationType.SMALL_IMAGE
-            ),
+            upperAndLowerComplicationTypes,
             DefaultComplicationProviderPolicy(SystemProviders.WORLD_CLOCK),
-            createBoundsRect(
-                UPPER_CIRCLE_COMPLICATION_CENTER_FRACTION,
-                CIRCLE_COMPLICATION_DIAMETER_FRACTION
+            ComplicationBounds(
+                ComplicationType.values().associateWith {
+                    if (it == ComplicationType.LONG_TEXT) {
+                        createBoundsRect(
+                            UPPER_ROUND_RECT_COMPLICATION_CENTER_FRACTION,
+                            ROUND_RECT_COMPLICATION_SIZE_FRACTION
+                        )
+                    } else {
+                        createBoundsRect(
+                            UPPER_CIRCLE_COMPLICATION_CENTER_FRACTION,
+                            CIRCLE_COMPLICATION_DIAMETER_FRACTION
+                        )
+                    }
+                }
             )
         ).setDefaultProviderType(ComplicationType.LONG_TEXT)
             .build()
         val lowerComplication = Complication.createRoundRectComplicationBuilder(
             ComplicationID.LOWER.ordinal,
             watchFaceStyle.getComplicationDrawableRenderer(this, watchState),
-            listOf(
-                ComplicationType.LONG_TEXT,
-                ComplicationType.RANGED_VALUE,
-                ComplicationType.SHORT_TEXT,
-                ComplicationType.MONOCHROMATIC_IMAGE,
-                ComplicationType.SMALL_IMAGE
-            ),
+            upperAndLowerComplicationTypes,
             DefaultComplicationProviderPolicy(SystemProviders.NEXT_EVENT),
-            createBoundsRect(
-                LOWER_CIRCLE_COMPLICATION_CENTER_FRACTION,
-                CIRCLE_COMPLICATION_DIAMETER_FRACTION
+            ComplicationBounds(
+                ComplicationType.values().associateWith {
+                    if (it == ComplicationType.LONG_TEXT) {
+                        createBoundsRect(
+                            LOWER_ROUND_RECT_COMPLICATION_CENTER_FRACTION,
+                            ROUND_RECT_COMPLICATION_SIZE_FRACTION
+                        )
+                    } else {
+                        createBoundsRect(
+                            LOWER_CIRCLE_COMPLICATION_CENTER_FRACTION,
+                            CIRCLE_COMPLICATION_DIAMETER_FRACTION
+                        )
+                    }
+                }
             )
         ).setDefaultProviderType(ComplicationType.LONG_TEXT)
             .build()
@@ -569,36 +610,12 @@
             colorStyleSetting,
             complicationsManager
         )
-
-        // Make the upper and lower complications change shape depending on the complication's type.
         upperComplication.complicationData.addObserver {
-            if (it.type == ComplicationType.LONG_TEXT) {
-                upperComplication.unitSquareBounds = createBoundsRect(
-                    UPPER_ROUND_RECT_COMPLICATION_CENTER_FRACTION,
-                    ROUND_RECT_COMPLICATION_SIZE_FRACTION
-                )
-            } else {
-                upperComplication.unitSquareBounds = createBoundsRect(
-                    UPPER_CIRCLE_COMPLICATION_CENTER_FRACTION,
-                    CIRCLE_COMPLICATION_DIAMETER_FRACTION
-                )
-            }
             // Force bounds recalculation, because this can affect the size of the central time
             // display.
             renderer.oldBounds.set(0, 0, 0, 0)
         }
         lowerComplication.complicationData.addObserver {
-            if (it.type == ComplicationType.LONG_TEXT) {
-                lowerComplication.unitSquareBounds = createBoundsRect(
-                    LOWER_ROUND_RECT_COMPLICATION_CENTER_FRACTION,
-                    ROUND_RECT_COMPLICATION_SIZE_FRACTION
-                )
-            } else {
-                lowerComplication.unitSquareBounds = createBoundsRect(
-                    LOWER_CIRCLE_COMPLICATION_CENTER_FRACTION,
-                    CIRCLE_COMPLICATION_DIAMETER_FRACTION
-                )
-            }
             // Force bounds recalculation, because this can affect the size of the central time
             // display.
             renderer.oldBounds.set(0, 0, 0, 0)
@@ -620,7 +637,7 @@
     private val watchState: WatchState,
     private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
     private val complicationsManager: ComplicationsManager
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
     surfaceHolder,
     userStyleRepository,
     watchState,
@@ -1050,9 +1067,9 @@
         }
         canvas.drawColor(
             getRGBColor(
-                Color.valueOf(backgroundColor),
+                backgroundColor,
                 drawProperties.backgroundAlpha,
-                Color.valueOf(Color.BLACK)
+                Color.BLACK
             )
         )
     }
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 458071d..b6f1782 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -27,15 +27,16 @@
 import android.util.Log
 import android.view.Gravity
 import android.view.SurfaceHolder
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.DrawMode
-import androidx.wear.watchface.GlesRenderer
 import androidx.wear.watchface.GlesTextureComplication
 import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -122,7 +123,7 @@
                     ComplicationType.SMALL_IMAGE
                 ),
                 DefaultComplicationProviderPolicy(SystemProviders.DAY_OF_WEEK),
-                RectF(0.2f, 0.7f, 0.4f, 0.9f)
+                ComplicationBounds(RectF(0.2f, 0.7f, 0.4f, 0.9f))
             ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
                 .build()
         ),
@@ -155,7 +156,7 @@
     watchState: WatchState,
     private val colorStyleSetting: ListUserStyleSetting,
     private val complication: Complication
-) : GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
+) : Renderer.GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
 
     /** Projection transformation matrix. Converts from 3D to 2D.  */
     private val projectionMatrix = FloatArray(16)
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index 8da96bb..ab10e84 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -24,15 +24,16 @@
 import android.icu.util.Calendar
 import android.view.SurfaceHolder
 import androidx.annotation.Sampled
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
-import androidx.wear.watchface.CanvasRenderer
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.CanvasComplicationDrawable
 import androidx.wear.watchface.ComplicationsManager
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -117,7 +118,7 @@
                             ComplicationType.SMALL_IMAGE
                         ),
                         DefaultComplicationProviderPolicy(SystemProviders.DAY_OF_WEEK),
-                        RectF(0.15625f, 0.1875f, 0.84375f, 0.3125f)
+                        ComplicationBounds(RectF(0.15625f, 0.1875f, 0.84375f, 0.3125f))
                     ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
                         .build(),
                     Complication.createRoundRectComplicationBuilder(
@@ -134,14 +135,14 @@
                             ComplicationType.SMALL_IMAGE
                         ),
                         DefaultComplicationProviderPolicy(SystemProviders.STEP_COUNT),
-                        RectF(0.1f, 0.5625f, 0.35f, 0.8125f)
+                        ComplicationBounds(RectF(0.1f, 0.5625f, 0.35f, 0.8125f))
                     ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
                         .build()
                 ),
                 userStyleRepository
             )
 
-            val renderer = object : CanvasRenderer(
+            val renderer = object : Renderer.CanvasRenderer(
                 surfaceHolder,
                 userStyleRepository,
                 watchState,
diff --git a/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml b/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml
deleted file mode 100644
index ed1e13d..0000000
--- a/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2020 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<resources>
-    <string name="app_name" translatable="false">Example Watchfaces</string>
-    <string name="canvas_analog_watch_face_name"
-        translatable="false">Example Analog Watchface</string>
-    <string name="canvas_digital_watch_face_name"
-        translatable="false">Example Digital Watchface</string>
-    <string name="gl_watch_face_name" translatable="false">Example OpenGL Watchface</string>
-
-    <!-- Name of watchface style [CHAR LIMIT=80] -->
-    <string name="red_style_name">Red Style</string>
-
-    <!-- Name of watchface style [CHAR LIMIT=80] -->
-    <string name="green_style_name">Green Style</string>
-</resources>
diff --git a/wear/wear-watchface/samples/src/main/res/values/strings.xml b/wear/wear-watchface/samples/src/main/res/values/strings.xml
new file mode 100644
index 0000000..90f3dd8
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/values/strings.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <string name="app_name" translatable="false">Example Watchfaces</string>
+    <string name="canvas_analog_watch_face_name"
+        translatable="false">Example Analog Watchface</string>
+    <string name="canvas_digital_watch_face_name"
+        translatable="false">Example Digital Watchface</string>
+    <string name="gl_watch_face_name" translatable="false">Example OpenGL Watchface</string>
+
+    <!-- Name of watchface style [CHAR LIMIT=20] -->
+    <string name="red_style_name">Red Style</string>
+
+    <!-- Name of watchface style [CHAR LIMIT=20] -->
+    <string name="green_style_name">Green Style</string>
+
+    <!-- Name of watchface style category for selecting the color theme [CHAR LIMIT=20] -->
+    <string name="colors_style_setting">Colors</string>
+
+    <!-- Subtitle for the menu option to select the watch face color theme [CHAR LIMIT=20] -->
+    <string name="colors_style_setting_description">Watchface colorization</string>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_red">Red</string>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_green">Green</string>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_blue">Blue</string>
+
+    <!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
+    <string name="watchface_pips_setting">Hour Pips</string>
+
+    <!-- Subtitle for the menu option to configure if we should draw pips to mark each hour
+     on the watch face [CHAR LIMIT=20] -->
+    <string name="watchface_pips_setting_description">Whether to draw or not</string>
+
+    <!-- A menu option to select a widget that lets us configure the length of the watch hands
+    [CHAR LIMIT=20] -->
+    <string name="watchface_hand_length_setting">Hand length</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the length of the
+    watch hands [CHAR LIMIT=20] -->
+    <string name="watchface_hand_length_setting_description">Scale of watch hands</string>
+
+    <!-- A menu option to select a widget that lets us configure the Complications (a watch
+    making term) [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting">Complications</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the Complications
+    (a watch making term) [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_description">Number and position</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select both
+    the left and the right complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_both">Both</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select no
+    complications for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_none">None</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select only the left
+    complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_left">Left</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select only the
+    right complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_right">Right</string>
+</resources>
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
index 98f48df..26d6d79 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
@@ -31,7 +31,7 @@
     private val handler: Handler,
     var mockSystemTimeMillis: Long,
     var surfaceHolderOverride: SurfaceHolder,
-    var userUnlocked: Boolean
+    var preRInitFlow: Boolean
 ) : WatchFaceService() {
 
     private val mutableWatchState = MutableWatchState().apply {
@@ -68,5 +68,5 @@
 
     override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
 
-    override fun isUserUnlocked() = userUnlocked
+    override fun expectPreRInitFlow() = preRInitFlow
 }
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index 0e28d2f..95a9ebb 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -19,6 +19,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Color
 import android.support.wearable.watchface.SharedMemoryImage
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -88,7 +89,8 @@
                     RenderParameters(
                         DrawMode.INTERACTIVE,
                         RenderParameters.DRAW_ALL_LAYERS,
-                        null
+                        null,
+                        Color.RED
                     ).toWireFormat(),
                     100,
                     1234567890,
@@ -128,7 +130,8 @@
                     RenderParameters(
                         DrawMode.AMBIENT,
                         RenderParameters.DRAW_ALL_LAYERS,
-                        null
+                        null,
+                        Color.RED
                     ).toWireFormat(),
                     100,
                     123456789,
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 5042dba..6d0daab 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Rect
 import android.graphics.SurfaceTexture
 import android.os.Handler
@@ -46,8 +47,10 @@
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.GREEN_STYLE
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import org.junit.After
@@ -246,7 +249,8 @@
                         RenderParameters(
                             DrawMode.AMBIENT,
                             RenderParameters.DRAW_ALL_LAYERS,
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -279,7 +283,8 @@
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             RenderParameters.DRAW_ALL_LAYERS,
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -304,7 +309,7 @@
         initLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)
         handler.post {
             interactiveWatchFaceInstanceWCS.setCurrentUserStyle(
-                UserStyleWireFormat(mapOf("color_style_setting" to "green_style"))
+                UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
             )
             engineWrapper.draw()
         }
@@ -331,7 +336,8 @@
                                 Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
                                 Layer.TOP_LAYER to LayerMode.DRAW
                             ),
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -368,7 +374,8 @@
                                 Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
                                 Layer.TOP_LAYER to LayerMode.DRAW
                             ),
-                            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+                            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -416,7 +423,8 @@
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             RenderParameters.DRAW_ALL_LAYERS,
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -442,15 +450,15 @@
         handler.post {
             // Change the style
             interactiveWatchFaceInstanceWCS.setCurrentUserStyle(
-                UserStyleWireFormat(mapOf("color_style_setting" to "green_style"))
+                UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
             )
 
             // Simulate device shutting down.
             InteractiveInstanceManager.deleteInstance(INTERACTIVE_INSTANCE_ID)
 
-            // Simulate a direct boot scenario where a new service is created with a locked user
-            // but there's no pending PendingWallpaperInteractiveWatchFaceInstance and no
-            // wallpaper command. This should load the direct boot parameters which get saved.
+            // Simulate a R style direct boot scenario where a new service is created but there's no
+            // pending PendingWallpaperInteractiveWatchFaceInstance and no wallpaper command. This
+            // should load the direct boot parameters which get saved.
             val service2 = TestCanvasAnalogWatchFaceService(
                 ApplicationProvider.getApplicationContext<Context>(),
                 handler,
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/BroadcastReceivers.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/BroadcastReceivers.kt
new file mode 100644
index 0000000..78f928f
--- /dev/null
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/BroadcastReceivers.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
+
+/**
+ * All watchface instances share the same [Context] which is a problem for broadcast receivers
+ * because the OS will mistakenly believe we're leaking them if there's more than one instance. So
+ * we need to use this class to share them.
+ */
+internal class BroadcastReceivers private constructor(private val context: Context) {
+
+    interface BroadcastEventObserver {
+        /** Called when we receive Intent.ACTION_TIME_TICK. */
+        @UiThread
+        fun onActionTimeTick()
+
+        /** Called when we receive Intent.ACTION_TIMEZONE_CHANGED. */
+        @UiThread
+        fun onActionTimeZoneChanged()
+
+        /** Called when we receive Intent.ACTION_TIME_CHANGED. */
+        @UiThread
+        fun onActionTimeChanged()
+
+        /** Called when we receive Intent.ACTION_BATTERY_CHANGED. */
+        @UiThread
+        fun onActionBatteryChanged(intent: Intent)
+
+        /** Called when we receive Intent.MOCK_TIME_INTENT. */
+        @UiThread
+        fun onMockTime(intent: Intent)
+    }
+
+    companion object {
+        val broadcastEventObservers = HashSet<BroadcastEventObserver>()
+
+        /* We don't leak due to balanced calls to[addBroadcastEventObserver] and
+        [removeBroadcastEventObserver] which sets this back to null.
+         */
+        @SuppressWarnings("StaticFieldLeak")
+        var broadcastReceivers: BroadcastReceivers? = null
+
+        @UiThread
+        fun addBroadcastEventObserver(context: Context, observer: BroadcastEventObserver) {
+            broadcastEventObservers.add(observer)
+            if (broadcastReceivers == null) {
+                broadcastReceivers = BroadcastReceivers(context)
+            }
+        }
+
+        @UiThread
+        fun removeBroadcastEventObserver(observer: BroadcastEventObserver) {
+            broadcastEventObservers.remove(observer)
+            if (broadcastEventObservers.isEmpty()) {
+                broadcastReceivers!!.onDestroy()
+                broadcastReceivers = null
+            }
+        }
+
+        @VisibleForTesting
+        fun sendOnActionBatteryChangedForTesting(intent: Intent) {
+            require(broadcastEventObservers.isNotEmpty())
+            for (observer in broadcastEventObservers) {
+                observer.onActionBatteryChanged(intent)
+            }
+        }
+
+        @VisibleForTesting
+        fun sendOnMockTimeForTesting(intent: Intent) {
+            require(broadcastEventObservers.isNotEmpty())
+            for (observer in broadcastEventObservers) {
+                observer.onMockTime(intent)
+            }
+        }
+    }
+
+    private val actionTimeTickReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        @SuppressWarnings("SyntheticAccessor")
+        override fun onReceive(context: Context, intent: Intent) {
+            for (observer in broadcastEventObservers) {
+                observer.onActionTimeTick()
+            }
+        }
+    }
+
+    private val actionTimeZoneReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            for (observer in broadcastEventObservers) {
+                observer.onActionTimeZoneChanged()
+            }
+        }
+    }
+
+    private val actionTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            for (observer in broadcastEventObservers) {
+                observer.onActionTimeChanged()
+            }
+        }
+    }
+
+    private val actionBatteryLevelReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        @SuppressWarnings("SyntheticAccessor")
+        override fun onReceive(context: Context, intent: Intent) {
+            for (observer in broadcastEventObservers) {
+                observer.onActionBatteryChanged(intent)
+            }
+        }
+    }
+
+    private val mockTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        @SuppressWarnings("SyntheticAccessor")
+        override fun onReceive(context: Context, intent: Intent) {
+            for (observer in broadcastEventObservers) {
+                observer.onMockTime(intent)
+            }
+        }
+    }
+
+    init {
+        context.registerReceiver(actionTimeTickReceiver, IntentFilter(Intent.ACTION_TIME_TICK))
+        context.registerReceiver(
+            actionTimeZoneReceiver,
+            IntentFilter(Intent.ACTION_TIMEZONE_CHANGED)
+        )
+        context.registerReceiver(actionTimeReceiver, IntentFilter(Intent.ACTION_TIME_CHANGED))
+        context.registerReceiver(
+            actionBatteryLevelReceiver,
+            IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+        )
+        context.registerReceiver(mockTimeReceiver, IntentFilter(WatchFaceImpl.MOCK_TIME_INTENT))
+    }
+
+    fun onDestroy() {
+        context.unregisterReceiver(actionTimeTickReceiver)
+        context.unregisterReceiver(actionTimeZoneReceiver)
+        context.unregisterReceiver(actionTimeReceiver)
+        context.unregisterReceiver(actionBatteryLevelReceiver)
+        context.unregisterReceiver(mockTimeReceiver)
+    }
+}
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt
deleted file mode 100644
index 3b81fea..0000000
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Rect
-import android.icu.util.Calendar
-import android.view.SurfaceHolder
-import androidx.annotation.IntDef
-import androidx.annotation.IntRange
-import androidx.annotation.UiThread
-import androidx.wear.watchface.style.UserStyleRepository
-
-/** @hide */
-@IntDef(
-    value = [
-        CanvasType.SOFTWARE,
-        CanvasType.HARDWARE
-    ]
-)
-public annotation class CanvasType {
-    public companion object {
-        /** A software canvas will be requested. */
-        public const val SOFTWARE: Int = 0
-
-        /**
-         * A hardware canvas will be requested. This is usually faster than software rendering,
-         * however it can sometimes increase battery usage by rendering at a higher frame rate.
-         */
-        public const val HARDWARE: Int = 1
-    }
-}
-
-/**
- * Watch faces that require [Canvas] rendering should extend their [Renderer] from this
- * class.
- */
-public abstract class CanvasRenderer(
-    /** The [SurfaceHolder] that [render] will draw into. */
-    surfaceHolder: SurfaceHolder,
-
-    /** The associated [UserStyleRepository]. */
-    userStyleRepository: UserStyleRepository,
-
-    /** The associated [WatchState]. */
-    watchState: WatchState,
-
-    /** The type of canvas to use. */
-    @CanvasType private val canvasType: Int,
-
-    /**
-     * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
-     * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces are
-     * recommended to use lower frame rates if possible for better battery life. Variable frame
-     * rates can also help preserve battery life, e.g. if a watch face has a short animation once
-     * per second it can adjust the frame rate inorder to sleep when not animating.
-     */
-    @IntRange(from = 0, to = 10000)
-    interactiveDrawModeUpdateDelayMillis: Long
-) : Renderer(surfaceHolder, userStyleRepository, watchState, interactiveDrawModeUpdateDelayMillis) {
-
-    internal override fun renderInternal(
-        calendar: Calendar
-    ) {
-        val canvas = (
-            if (canvasType == CanvasType.HARDWARE) {
-                surfaceHolder.lockHardwareCanvas()
-            } else {
-                surfaceHolder.lockCanvas()
-            }
-            ) ?: return
-        try {
-            if (watchState.isVisible.value) {
-                render(canvas, surfaceHolder.surfaceFrame, calendar)
-            } else {
-                canvas.drawColor(Color.BLACK)
-            }
-        } finally {
-            surfaceHolder.unlockCanvasAndPost(canvas)
-        }
-    }
-
-    /** {@inheritDoc} */
-    internal override fun takeScreenshot(
-        calendar: Calendar,
-        renderParameters: RenderParameters
-    ): Bitmap {
-        val bitmap = Bitmap.createBitmap(
-            screenBounds.width(),
-            screenBounds.height(),
-            Bitmap.Config.ARGB_8888
-        )
-        val prevRenderParameters = this.renderParameters
-        this.renderParameters = renderParameters
-        render(Canvas(bitmap), screenBounds, calendar)
-        this.renderParameters = prevRenderParameters
-        return bitmap
-    }
-
-    /**
-     * Sub-classes should override this to implement their rendering logic which should respect
-     * the current [DrawMode]. For correct functioning watch faces must use the supplied
-     * [Calendar] and avoid using any other ways of getting the time.
-     *
-     * @param canvas The [Canvas] to render into. Don't assume this is always the canvas from
-     *     the [SurfaceHolder] backing the display
-     * @param bounds A [Rect] describing the bonds of the canvas to draw into
-     * @param calendar The current [Calendar]
-     */
-    @UiThread
-    public abstract fun render(
-        canvas: Canvas,
-        bounds: Rect,
-        calendar: Calendar
-    )
-}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 374b3fb..eb36e4a 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -23,33 +23,40 @@
 import android.graphics.drawable.Drawable
 import android.icu.util.Calendar
 import android.support.wearable.complications.ComplicationData
+import androidx.annotation.ColorInt
 import androidx.annotation.UiThread
+import androidx.wear.complications.ComplicationBounds
+import androidx.wear.complications.ComplicationHelperActivity
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.complications.data.IdAndComplicationData
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
 import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.style.Layer
+import androidx.wear.watchface.style.UserStyleSetting
 
-/** Common interface for rendering complications onto a [Canvas]. */
+/** Interface for rendering complications onto a [Canvas]. */
 public interface CanvasComplication {
     /**
-     * Called when the CanvasComplication attaches to a [Complication].
+     * Called when the CanvasComplication attaches to a [Complication]. This will get called during
+     * [Complication] initialization and if [Complication.renderer] is assigned with this
+     * CanvasComplication.
      */
     @UiThread
     public fun onAttach(complication: Complication)
 
     /**
-     * Called when the CanvasComplication detaches from a [Complication].
+     * Called when the CanvasComplication detaches from a [Complication]. This will get called if
+     * [Complication.renderer] is assigned to a different CanvasComplication.
      */
     @UiThread
     public fun onDetach()
 
     /**
-     * Draws the complication into the canvas with the specified bounds. This will usually be
-     * called by user watch face drawing code, but the system may also call it for complication
-     * selection UI rendering. The width and height will be the same as that computed by
-     * computeBounds but the translation and canvas size may differ.
+     * Draws the complication defined by [idAndData] into the canvas with the specified bounds. This
+     * will usually be called by user watch face drawing code, but the system may also call it
+     * for complication selection UI rendering. The width and height will be the same as that
+     * computed by computeBounds but the translation and canvas size may differ.
      *
      * @param canvas The [Canvas] to render into
      * @param bounds A [Rect] describing the bounds of the complication
@@ -65,8 +72,8 @@
     )
 
     /**
-     * Whether the complication should be drawn highlighted. This is to provide visual
-     * feedback when the user taps on a complication.
+     * Whether the complication should be drawn highlighted. This is to provide visual feedback when
+     * the user taps on a complication.
      */
     @Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
     @get:JvmName("isHighlighted")
@@ -78,13 +85,17 @@
 }
 
 /**
- * A complication rendered with [ComplicationDrawable] which renders complications in a
- * material design style. This renderer can't be shared by multiple complications.
+ * A complication rendered with [ComplicationDrawable] which renders complications in a material
+ * design style. This renderer can't be shared by multiple complications.
  */
 public open class CanvasComplicationDrawable(
     /** The [ComplicationDrawable] to render with. */
     drawable: ComplicationDrawable,
 
+    /**
+     * The watch's [WatchState] which contains details pertaining to (low-bit) ambient mode and
+     * burn in protection needed to render correctly.
+     */
     private val watchState: WatchState
 ) : CanvasComplication {
 
@@ -153,25 +164,30 @@
                 if (renderParameters.highlightedComplicationId == null ||
                     renderParameters.highlightedComplicationId == idAndData?.complicationId
                 ) {
-                    drawHighlight(canvas, bounds, calendar)
+                    drawOutline(canvas, bounds, calendar, renderParameters.highlightTint)
                 }
             }
             LayerMode.HIDE -> return
         }
     }
 
-    /** Used (indirectly) by the editor, draws a highlight around the complication. */
-    public open fun drawHighlight(
+    /** Used (indirectly) by the editor, draws a dashed line around the complication. */
+    public open fun drawOutline(
         canvas: Canvas,
         bounds: Rect,
-        calendar: Calendar
+        calendar: Calendar,
+        @ColorInt color: Int
     ) {
-        ComplicationOutlineRenderer.drawComplicationSelectOutline(canvas, bounds)
+        ComplicationOutlineRenderer.drawComplicationSelectOutline(
+            canvas,
+            bounds,
+            color
+        )
     }
 
     /**
-     * Whether or not the complication should be drawn highlighted. Used to provide visual
-     * feedback when the complication is tapped.
+     * Whether or not the complication should be drawn highlighted. Used to provide visual feedback
+     * when the complication is tapped.
      */
     override var isHighlighted: Boolean
         @Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
@@ -185,9 +201,7 @@
             drawable.isHighlighted = value
         }
 
-    /**
-     * The [IdAndComplicationData] to use when rendering the complication.
-     */
+    /** The [IdAndComplicationData] to use when rendering the complication. */
     override var idAndData: IdAndComplicationData? = null
         @UiThread
         set(value) {
@@ -198,12 +212,13 @@
 
 /**
  * Represents a individual complication on the screen. The number of complications is fixed
- * (see [ComplicationsManager]) but complications can be enabled or disabled as needed.
+ * (see [ComplicationsManager]) but complications can be enabled or disabled via
+ * [UserStyleSetting.ComplicationsUserStyleSetting].
  */
 public class Complication internal constructor(
     internal val id: Int,
     @ComplicationBoundsType internal val boundsType: Int,
-    unitSquareBounds: RectF,
+    complicationBounds: ComplicationBounds,
     canvasComplication: CanvasComplication,
     supportedTypes: List<ComplicationType>,
     defaultProviderPolicy: DefaultComplicationProviderPolicy,
@@ -220,44 +235,41 @@
          */
         @JvmStatic
         public fun createRoundRectComplicationBuilder(
-            /** The watch face's ID for this complication. */
+            /**
+             * The watch face's ID for this complication. Can be any integer but should be unique
+             * within the watch face.
+             */
             id: Int,
 
             /**
-             * The renderer for this Complication. Renderers may not be sharable between complications.
+             * The [CanvasComplication] to use for rendering. Note renderers should not be shared
+             * between complications.
              */
             renderer: CanvasComplication,
 
             /**
              * The types of complication supported by this Complication. Passed into
              * [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
-             * configuration.
+             * configuration. This list should be non-empty.
              */
             supportedTypes: List<ComplicationType>,
 
-            /** The [DefaultComplicationProviderPolicy] to use. */
+            /**
+             * The [DefaultComplicationProviderPolicy] used to select the initial complication
+             * provider.
+             */
             defaultProviderPolicy: DefaultComplicationProviderPolicy,
 
-            /**
-             * The fractional bounds for the complication which are clamped to the unit square
-             * [0..1], and subsequently converted to screen space coordinates. NB 0 and 1 are
-             * included in the unit square.
-             */
-            unitSquareBounds: RectF
+            /** The initial [ComplicationBounds]. */
+            complicationBounds: ComplicationBounds
         ): Builder = Builder(
             id,
             renderer,
             supportedTypes,
             defaultProviderPolicy,
             ComplicationBoundsType.ROUND_RECT,
-            RectF().apply {
-                setIntersect(
-                    unitSquareBounds,
-                    unitSquare
-                )
-            }
+            complicationBounds
         )
-
         /**
          * Constructs a [Builder] for a complication with bound type
          * [ComplicationBoundsType.BACKGROUND] whose bounds cover the entire screen. A background
@@ -267,22 +279,29 @@
          */
         @JvmStatic
         public fun createBackgroundComplicationBuilder(
-            /** The watch face's ID for this complication. */
+            /**
+             * The watch face's ID for this complication. Can be any integer but should be unique
+             * within the watch face.
+             */
             id: Int,
 
             /**
-             * The renderer for this Complication. Renderers may not be sharable between complications.
+             * The [CanvasComplication] to use for rendering. Note renderers should not be shared
+             * between complications.
              */
             renderer: CanvasComplication,
 
             /**
              * The types of complication supported by this Complication. Passed into
              * [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
-             * configuration.
+             * configuration. This list should be non-empty.
              */
             supportedTypes: List<ComplicationType>,
 
-            /** The [DefaultComplicationProviderPolicy] to use. */
+            /**
+             * The [DefaultComplicationProviderPolicy] used to select the initial complication
+             * provider.
+             */
             defaultProviderPolicy: DefaultComplicationProviderPolicy
         ): Builder = Builder(
             id,
@@ -290,7 +309,7 @@
             supportedTypes,
             defaultProviderPolicy,
             ComplicationBoundsType.BACKGROUND,
-            RectF(0f, 0f, 1f, 1f)
+            ComplicationBounds(RectF(0f, 0f, 1f, 1f))
         )
     }
 
@@ -301,12 +320,14 @@
         private val supportedTypes: List<ComplicationType>,
         private val defaultProviderPolicy: DefaultComplicationProviderPolicy,
         @ComplicationBoundsType private val boundsType: Int,
-        private val unitSquareBounds: RectF
+        private val complicationBounds: ComplicationBounds
     ) {
         private var defaultProviderType = ComplicationType.NOT_CONFIGURED
 
         /**
-         * Sets the default complication provider data type.
+         * Sets the initial [ComplicationType] to use with the initial complication provider.
+         * Note care should be taken to ensure [defaultProviderType] is compatible with the
+         * [DefaultComplicationProviderPolicy].
          */
         public fun setDefaultProviderType(
             defaultProviderType: ComplicationType
@@ -319,7 +340,7 @@
         public fun build(): Complication = Complication(
             id,
             boundsType,
-            unitSquareBounds,
+            complicationBounds,
             renderer,
             supportedTypes,
             defaultProviderPolicy,
@@ -340,45 +361,44 @@
     private lateinit var complicationsManager: ComplicationsManager
     private lateinit var invalidateListener: InvalidateListener
 
-    private var _unitSquareBounds = unitSquareBounds
-    internal var unitSquareBoundsDirty = true
+    internal var complicationBoundsDirty = true
 
     /**
-     * The screen space unit-square bounds of the complication. This is converted to pixels during
-     * rendering.
+     * The complication's [ComplicationBounds] which are converted to pixels during rendering.
+     *
+     * Note it's not allowed to change the bounds of a background complication because
+     * they are assumed to always cover the entire screen.
      */
-    public var unitSquareBounds: RectF
+    public var complicationBounds: ComplicationBounds = complicationBounds
         @UiThread
-        get() = _unitSquareBounds
+        get
         @UiThread
         set(value) {
-            if (_unitSquareBounds == value) {
+            require(boundsType != ComplicationBoundsType.BACKGROUND)
+            if (field == value) {
                 return
             }
-            _unitSquareBounds = value
-            unitSquareBoundsDirty = true
+            field = value
+            complicationBoundsDirty = true
 
             // The caller might modify a number of complications. For efficiency we need to coalesce
             // these into one update task.
             complicationsManager.scheduleUpdate()
         }
 
-    private var _enabled = true
     internal var enabledDirty = true
 
-    /**
-     * Whether or not the complication should be drawn and accept taps.
-     */
-    public var enabled: Boolean
+    /** Whether or not the complication should be drawn and accept taps. */
+    public var enabled: Boolean = true
         @JvmName("isEnabled")
         @UiThread
-        get() = _enabled
+        get
         @UiThread
-        set(value) {
-            if (_enabled == value) {
+        internal set(value) {
+            if (field == value) {
                 return
             }
-            _enabled = value
+            field = value
             enabledDirty = true
 
             // The caller might enable/disable a number of complications. For efficiency we need
@@ -388,40 +408,34 @@
             }
         }
 
-    private var _renderer = canvasComplication
-
-    /**
-     * The [CanvasComplication] used to render the complication.
-     */
-    public var renderer: CanvasComplication
+    /** The [CanvasComplication] used to render the complication. */
+    public var renderer: CanvasComplication = canvasComplication
         @UiThread
-        get() = _renderer
+        get
         @UiThread
         set(value) {
-            if (_renderer == value) {
+            if (field == value) {
                 return
             }
             renderer.onDetach()
             value.idAndData = renderer.idAndData
-            _renderer = value
+            field = value
             value.onAttach(this)
         }
 
-    private var _supportedTypes = supportedTypes
     internal var supportedTypesDirty = true
 
-    /**
-     * The types of complications the complication supports.
-     */
-    public var supportedTypes: List<ComplicationType>
+    /** The types of complications the complication supports. Must be non-empty. */
+    public var supportedTypes: List<ComplicationType> = supportedTypes
         @UiThread
-        get() = _supportedTypes
+        get
         @UiThread
-        set(value) {
-            if (_supportedTypes == value) {
+        internal set(value) {
+            if (field == value) {
                 return
             }
-            _supportedTypes = value
+            require(value.isNotEmpty())
+            field = value
             supportedTypesDirty = true
 
             // The caller might modify a number of complications. For efficiency we need to
@@ -431,22 +445,21 @@
             }
         }
 
-    private var _defaultProviderPolicy = defaultProviderPolicy
     internal var defaultProviderPolicyDirty = true
 
     /**
      * The [DefaultComplicationProviderPolicy] which defines the default complications providers
-     * selected when the user hasn't yet made a choice. See also [.defaultProviderType].
+     * selected when the user hasn't yet made a choice. See also [defaultProviderType].
      */
-    public var defaultProviderPolicy: DefaultComplicationProviderPolicy
+    public var defaultProviderPolicy: DefaultComplicationProviderPolicy = defaultProviderPolicy
         @UiThread
-        get() = _defaultProviderPolicy
+        get
         @UiThread
-        set(value) {
-            if (_defaultProviderPolicy == value) {
+        internal set(value) {
+            if (field == value) {
                 return
             }
-            _defaultProviderPolicy = value
+            field = value
             defaultProviderPolicyDirty = true
 
             // The caller might modify a number of complications. For efficiency we need to
@@ -459,13 +472,13 @@
     internal var defaultProviderTypeDirty = true
 
     /**
-     * The default [ComplicationData.ComplicationType] to use alongside [.defaultProviderPolicy].
+     * The default [ComplicationData.ComplicationType] to use alongside [defaultProviderPolicy].
      */
     public var defaultProviderType: ComplicationType = defaultProviderType
         @UiThread
-        get() = field
+        get
         @UiThread
-        set(value) {
+        internal set(value) {
             if (field == value) {
                 return
             }
@@ -556,11 +569,19 @@
     }
 
     /** Computes the bounds of the complication by converting the unitSquareBounds to pixels. */
-    internal fun computeBounds(screen: Rect) =
-        Rect(
+    internal fun computeBounds(screen: Rect): Rect {
+        // Try the current type if there is one, otherwise fall back to the bounds for the default
+        // provider type.
+        val unitSquareBounds =
+            renderer.idAndData?.let {
+                complicationBounds.perComplicationTypeBounds[it.complicationData.type]
+            } ?: complicationBounds.perComplicationTypeBounds[defaultProviderType]!!
+        unitSquareBounds.intersect(unitSquare)
+        return Rect(
             (unitSquareBounds.left * screen.width()).toInt(),
             (unitSquareBounds.top * screen.height()).toInt(),
             (unitSquareBounds.right * screen.width()).toInt(),
             (unitSquareBounds.bottom * screen.height()).toInt()
         )
+    }
 }
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
index 57699ba..907b83e 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
@@ -17,9 +17,9 @@
 package androidx.wear.watchface
 
 import android.graphics.Canvas
-import android.graphics.Color
 import android.graphics.Paint
 import android.graphics.Rect
+import androidx.annotation.ColorInt
 import kotlin.math.cos
 import kotlin.math.sin
 
@@ -30,20 +30,24 @@
 public class ComplicationOutlineRenderer {
     public companion object {
         // Dashed lines are used for complication selection.
-        internal val DASH_WIDTH = 10.0f
-        internal var DASH_GAP = 2.0f
-        internal var DASH_LENGTH = 5.0f
+        internal const val DASH_WIDTH = 10.0f
+        internal const val DASH_GAP = 2.0f
+        internal const val DASH_LENGTH = 5.0f
 
         internal val dashPaint = Paint().apply {
             strokeWidth = DASH_WIDTH
             style = Paint.Style.FILL_AND_STROKE
             isAntiAlias = true
-            color = Color.RED
         }
 
         /** Draws a thick dotted line around the complication with the given bounds. */
         @JvmStatic
-        public fun drawComplicationSelectOutline(canvas: Canvas, bounds: Rect) {
+        public fun drawComplicationSelectOutline(
+            canvas: Canvas,
+            bounds: Rect,
+            @ColorInt color: Int
+        ) {
+            dashPaint.color = color
             if (bounds.width() == bounds.height()) {
                 drawCircleDashBorder(canvas, bounds)
                 return
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
index b9d9486..7dacf5c 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
@@ -20,12 +20,12 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.graphics.RectF
 import android.icu.util.Calendar
 import android.support.wearable.watchface.accessibility.AccessibilityUtils
 import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.ComplicationHelperActivity
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.data.ComplicationData
@@ -39,18 +39,15 @@
 
 private fun getComponentName(context: Context) = ComponentName(
     context.packageName,
-    context.javaClass.typeName
+    context.javaClass.name
 )
 
 /**
- * The [Complication]s associated with the [WatchFace]. Dynamic creation of
- * complications isn't supported, however complications can be enabled and disabled, perhaps as
- * part of a user style see [androidx.wear.watchface.style.UserStyleSetting].
+ * The [Complication]s associated with the [WatchFace]. Dynamic creation of complications isn't
+ * supported, however complications can be enabled and disabled by [ComplicationsUserStyleSetting].
  */
 public class ComplicationsManager(
-    /**
-     * The complications associated with the watch face, may be empty.
-     */
+    /** The complications associated with the watch face, may be empty. */
     complicationCollection: Collection<Complication>,
 
     /**
@@ -59,6 +56,10 @@
      */
     private val userStyleRepository: UserStyleRepository
 ) {
+    /**
+     * Interface used to report user taps on the complication. See [addTapListener] and
+     * [removeTapListener].
+     */
     public interface TapCallback {
         /**
          * Called when the user single taps on a complication.
@@ -81,13 +82,12 @@
     private lateinit var renderer: Renderer
     private lateinit var pendingUpdate: CancellableUniqueTask
 
-    // A map of IDs to complications.
+    /** A map of complication IDs to complications. */
     public val complications: Map<Int, Complication> =
         complicationCollection.associateBy(Complication::id)
 
     private class InitialComplicationConfig(
-        val id: Int,
-        val unitSquareBounds: RectF,
+        val complicationBounds: ComplicationBounds,
         val enabled: Boolean,
         val supportedTypes: List<ComplicationType>,
         val defaultProviderPolicy: DefaultComplicationProviderPolicy,
@@ -102,8 +102,7 @@
             { it.id },
             {
                 InitialComplicationConfig(
-                    it.id,
-                    it.unitSquareBounds,
+                    it.complicationBounds,
                     it.enabled,
                     it.supportedTypes,
                     it.defaultProviderPolicy,
@@ -175,8 +174,8 @@
             val override = styleOption.complicationOverlays.find { it.complicationId == id }
             val initialConfig = initialComplicationConfigs[id]!!
             // Apply styleOption overrides.
-            complication.unitSquareBounds =
-                override?.bounds ?: initialConfig.unitSquareBounds
+            complication.complicationBounds =
+                override?.complicationBounds ?: initialConfig.complicationBounds
             complication.enabled =
                 override?.enabled ?: initialConfig.enabled
             complication.supportedTypes =
@@ -188,7 +187,7 @@
         }
     }
 
-    /** Returns the [Complication] corresponding to id or null. */
+    /** Returns the [Complication] corresponding to [id], if there is one, or `null`. */
     public operator fun get(id: Int): Complication? = complications[id]
 
     internal fun scheduleUpdate() {
@@ -246,7 +245,8 @@
                 activeKeys.add(id)
 
                 labelsDirty =
-                    labelsDirty || complication.dataDirty || complication.unitSquareBoundsDirty
+                    labelsDirty || complication.dataDirty ||
+                    complication.complicationBoundsDirty
 
                 if (complication.defaultProviderPolicyDirty ||
                     complication.defaultProviderTypeDirty
@@ -260,7 +260,7 @@
                 }
 
                 complication.dataDirty = false
-                complication.unitSquareBoundsDirty = false
+                complication.complicationBoundsDirty = false
                 complication.supportedTypesDirty = false
                 complication.defaultProviderPolicyDirty = false
                 complication.defaultProviderTypeDirty = false
@@ -299,8 +299,8 @@
     }
 
     /**
-     * Brings attention to the complication by briefly highlighting it to provide visual
-     * feedback when the user has tapped on it.
+     * Brings attention to the complication by briefly highlighting it to provide visual feedback
+     * when the user has tapped on it.
      *
      * @param complicationId The watch face's ID of the complication to briefly highlight
      */
@@ -324,7 +324,7 @@
     }
 
     /**
-     * Returns the id of the complication at coordinates x, y or {@code null} if there isn't one.
+     * Returns the id of the complication at coordinates x, y or `null` if there isn't one.
      *
      * @param x The x coordinate of the point to perform a hit test
      * @param y The y coordinate of the point to perform a hit test
@@ -337,9 +337,9 @@
         }?.value
 
     /**
-     * Returns the background complication if there is one or {@code null} otherwise.
+     * Returns the background complication if there is one or `null` otherwise.
      *
-     * @return The background complication if there is one or {@code null} otherwise
+     * @return The background complication if there is one or `null` otherwise
      */
     public fun getBackgroundComplication(): Complication? =
         complications.entries.firstOrNull {
@@ -410,8 +410,7 @@
     }
 
     /**
-     * Adds a [TapCallback] which is called whenever the user interacts with a
-     * complication.
+     * Adds a [TapCallback] which is called whenever the user interacts with a complication.
      */
     @UiThread
     @SuppressLint("ExecutorRegistration")
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt
deleted file mode 100644
index 8e7cde4..0000000
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface
-
-import android.annotation.SuppressLint
-import android.graphics.Bitmap
-import android.icu.util.Calendar
-import android.opengl.EGL14
-import android.opengl.EGLConfig
-import android.opengl.EGLContext
-import android.opengl.EGLDisplay
-import android.opengl.EGLSurface
-import android.opengl.GLES20
-import android.util.Log
-import android.view.SurfaceHolder
-import androidx.annotation.CallSuper
-import androidx.annotation.IntRange
-import androidx.annotation.UiThread
-import androidx.wear.watchface.style.UserStyleRepository
-
-import java.nio.ByteBuffer
-
-internal val EGL_CONFIG_ATTRIB_LIST = intArrayOf(
-    EGL14.EGL_RENDERABLE_TYPE,
-    EGL14.EGL_OPENGL_ES2_BIT,
-    EGL14.EGL_RED_SIZE,
-    8,
-    EGL14.EGL_GREEN_SIZE,
-    8,
-    EGL14.EGL_BLUE_SIZE,
-    8,
-    EGL14.EGL_ALPHA_SIZE,
-    8,
-    EGL14.EGL_NONE
-)
-
-private val EGL_CONTEXT_ATTRIB_LIST =
-    intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
-
-internal val EGL_SURFACE_ATTRIB_LIST = intArrayOf(EGL14.EGL_NONE)
-
-/**
- * Watch faces that require [GLES20] rendering should extend their [Renderer] from this
- * class.
- */
-public abstract class GlesRenderer @JvmOverloads constructor(
-    /** The [SurfaceHolder] that [render] will draw into. */
-    surfaceHolder: SurfaceHolder,
-
-    /** The associated [UserStyleRepository]. */
-    userStyleRepository: UserStyleRepository,
-
-    /** The associated [WatchState]. */
-    watchState: WatchState,
-
-    /**
-     * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
-     * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces are
-     * recommended to use lower frame rates if possible for better battery life. Variable frame
-     * rates can also help preserve battery life, e.g. if a watch face has a short animation once
-     * per second it can adjust the frame rate inorder to sleep when not animating.
-     */
-    @IntRange(from = 0, to = 10000)
-    interactiveDrawModeUpdateDelayMillis: Long,
-
-    /** Attributes for [EGL14.eglChooseConfig]. By default this selects an RGBAB8888 back buffer. */
-    private val eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
-
-    /** The attributes to be passed to [EGL14.eglCreateWindowSurface]. By default this is empty. */
-    private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
-) : Renderer(surfaceHolder, userStyleRepository, watchState, interactiveDrawModeUpdateDelayMillis) {
-    /** @hide */
-    private companion object {
-        private const val TAG = "Gles2WatchFace"
-    }
-
-    private var eglDisplay: EGLDisplay? = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY).apply {
-        if (this == EGL14.EGL_NO_DISPLAY) {
-            throw RuntimeException("eglGetDisplay returned EGL_NO_DISPLAY")
-        }
-        // Initialize the display. The major and minor version numbers are passed back.
-        val version = IntArray(2)
-        if (!EGL14.eglInitialize(this, version, 0, version, 1)) {
-            throw RuntimeException("eglInitialize failed")
-        }
-    }
-
-    private var eglConfig: EGLConfig = chooseEglConfig(eglDisplay!!)
-
-    @SuppressWarnings("SyntheticAccessor")
-    private var eglContext: EGLContext? = EGL14.eglCreateContext(
-        eglDisplay,
-        eglConfig,
-        EGL14.EGL_NO_CONTEXT,
-        EGL_CONTEXT_ATTRIB_LIST,
-        0
-    )
-
-    init {
-        if (eglContext == EGL14.EGL_NO_CONTEXT) {
-            throw RuntimeException("eglCreateContext failed")
-        }
-    }
-
-    private var eglSurface: EGLSurface? = null
-    private var calledOnGlContextCreated = false
-
-    /**
-     * Chooses the EGLConfig to use.
-     * @throws RuntimeException if [EGL14.eglChooseConfig] fails
-     */
-    private fun chooseEglConfig(eglDisplay: EGLDisplay): EGLConfig {
-        val numEglConfigs = IntArray(1)
-        val eglConfigs = arrayOfNulls<EGLConfig>(1)
-        if (!EGL14.eglChooseConfig(
-                eglDisplay,
-                eglConfigAttribList,
-                0,
-                eglConfigs,
-                0,
-                eglConfigs.size,
-                numEglConfigs,
-                0
-            )
-        ) {
-            throw RuntimeException("eglChooseConfig failed")
-        }
-        if (numEglConfigs[0] == 0) {
-            throw RuntimeException("no matching EGL configs")
-        }
-        return eglConfigs[0]!!
-    }
-
-    private fun createWindowSurface(width: Int, height: Int) {
-        if (eglSurface != null) {
-            if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
-                Log.w(TAG, "eglDestroySurface failed")
-            }
-        }
-        eglSurface = EGL14.eglCreateWindowSurface(
-            eglDisplay,
-            eglConfig,
-            surfaceHolder.surface,
-            eglSurfaceAttribList,
-            0
-        )
-        if (eglSurface == EGL14.EGL_NO_SURFACE) {
-            throw RuntimeException("eglCreateWindowSurface failed")
-        }
-
-        makeContextCurrent()
-        GLES20.glViewport(0, 0, width, height)
-        if (!calledOnGlContextCreated) {
-            calledOnGlContextCreated = true
-            onGlContextCreated()
-        }
-        onGlSurfaceCreated(width, height)
-    }
-
-    @CallSuper
-    override fun onDestroy() {
-        if (eglSurface != null) {
-            if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
-                Log.w(TAG, "eglDestroySurface failed")
-            }
-            eglSurface = null
-        }
-        if (eglContext != null) {
-            if (!EGL14.eglDestroyContext(eglDisplay, eglContext)) {
-                Log.w(TAG, "eglDestroyContext failed")
-            }
-            eglContext = null
-        }
-        if (eglDisplay != null) {
-            if (!EGL14.eglTerminate(eglDisplay)) {
-                Log.w(TAG, "eglTerminate failed")
-            }
-            eglDisplay = null
-        }
-    }
-
-    /**
-     * Sets our GL context to be the current one. This method *must* be called before any
-     * OpenGL APIs are used.
-     */
-    private fun makeContextCurrent() {
-        if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
-            throw RuntimeException("eglMakeCurrent failed")
-        }
-    }
-
-    internal override fun onPostCreate() {
-        surfaceHolder.addCallback(object : SurfaceHolder.Callback {
-            @SuppressLint("SyntheticAccessor")
-            override fun surfaceChanged(
-                holder: SurfaceHolder,
-                format: Int,
-                width: Int,
-                height: Int
-            ) {
-                createWindowSurface(width, height)
-            }
-
-            @SuppressLint("SyntheticAccessor")
-            override fun surfaceDestroyed(holder: SurfaceHolder) {
-                if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
-                    Log.w(TAG, "eglDestroySurface failed")
-                }
-                eglSurface = null
-            }
-
-            override fun surfaceCreated(holder: SurfaceHolder) {
-            }
-        })
-
-        createWindowSurface(
-            surfaceHolder.surfaceFrame.width(),
-            surfaceHolder.surfaceFrame.height()
-        )
-    }
-
-    /** Called when a new GL context is created. It's safe to use GL APIs in this method. */
-    @UiThread
-    public open fun onGlContextCreated() {}
-
-    /**
-     * Called when a new GL surface is created. It's safe to use GL APIs in this method.
-     *
-     * @param width width of surface in pixels
-     * @param height height of surface in pixels
-     */
-    @UiThread
-    public open fun onGlSurfaceCreated(width: Int, height: Int) {}
-
-    internal override fun renderInternal(
-        calendar: Calendar
-    ) {
-        makeContextCurrent()
-        render(calendar)
-        if (!EGL14.eglSwapBuffers(eglDisplay, eglSurface)) {
-            Log.w(TAG, "eglSwapBuffers failed")
-        }
-    }
-
-    /** {@inheritDoc} */
-    internal override fun takeScreenshot(
-        calendar: Calendar,
-        renderParameters: RenderParameters
-    ): Bitmap {
-        val width = screenBounds.width()
-        val height = screenBounds.height()
-        val pixelBuf = ByteBuffer.allocateDirect(width * height * 4)
-        makeContextCurrent()
-        val prevRenderParameters = this.renderParameters
-        this.renderParameters = renderParameters
-        render(calendar)
-        this.renderParameters = prevRenderParameters
-        GLES20.glFinish()
-        GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuf)
-        // The image is flipped when using read pixels because the first pixel in the OpenGL buffer
-        // is in bottom left.
-        verticalFlip(pixelBuf, width, height)
-        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
-        bitmap.copyPixelsFromBuffer(pixelBuf)
-        return bitmap
-    }
-
-    private fun verticalFlip(
-        buffer: ByteBuffer,
-        width: Int,
-        height: Int
-    ) {
-        var i = 0
-        val tmp = ByteArray(width * 4)
-        while (i++ < height / 2) {
-            buffer[tmp]
-            System.arraycopy(
-                buffer.array(),
-                buffer.limit() - buffer.position(),
-                buffer.array(),
-                buffer.position() - width * 4,
-                width * 4
-            )
-            System.arraycopy(tmp, 0, buffer.array(), buffer.limit() - buffer.position(), width * 4)
-        }
-        buffer.rewind()
-    }
-
-    /**
-     * Sub-classes should override this to implement their rendering logic which should respect
-     * the current [DrawMode]. For correct functioning watch faces must use the supplied
-     * [Calendar] and avoid using any other ways of getting the time.
-     *
-     * @param calendar The current [Calendar]
-     */
-    @UiThread
-    public abstract fun render(calendar: Calendar)
-}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
index 1a8cf62..4caca47 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
@@ -25,7 +25,10 @@
 import android.opengl.GLUtils
 import androidx.annotation.Px
 
-/** Helper for rendering a complication to a GLES20 texture. */
+/**
+ * Helper for rendering a [CanvasComplication] to a GLES20 texture. To use call [renderToTexture]
+ * and then [bind] before drawing.
+ */
 public class GlesTextureComplication(
     /** The [CanvasComplication] to render to texture. */
     public val canvasComplication: CanvasComplication,
@@ -50,7 +53,7 @@
     private val canvas = Canvas(bitmap)
     private val bounds = Rect(0, 0, textureWidth, textureHeight)
 
-    /** Renders the complication to an OpenGL texture. */
+    /** Renders [canvasComplication] to an OpenGL texture. */
     public fun renderToTexture(calendar: Calendar, renderParameters: RenderParameters) {
         canvas.drawColor(Color.BLACK)
         canvasComplication.render(canvas, bounds, calendar, renderParameters)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
index d7f7ada..91e83df 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
@@ -19,9 +19,9 @@
 import androidx.annotation.UiThread
 
 /**
- * An observable UI thread only data holder class.
+ * An observable UI thread only data holder class (see [Observer]).
  *
- * @param <T> The type of data hold by this instance
+ * @param T The type of data held by this instance
  */
 public open class ObservableWatchData<T : Any> internal constructor(internal var _value: T?) {
 
@@ -30,11 +30,13 @@
     private val toBeRemoved = HashSet<Observer<T>>()
 
     /** Whether or not this ObservableWatchData contains a value. */
+    @UiThread
     public fun hasValue(): Boolean = _value != null
 
     /**
      * Returns the value contained within this ObservableWatchData or default if there isn't one.
      */
+    @UiThread
     public fun getValueOr(default: T): T = if (_value != null) {
         _value!!
     } else {
@@ -67,9 +69,9 @@
         }
 
     /**
-     * Adds the given observer to the observers list. The events are dispatched on the ui thread.
-     * If there's any data held within the ObservableWatchData it will be immediately delivered to
-     * the observer.
+     * Adds the given [Observer] to the observers list. If [hasValue] would return true then
+     * [Observer.onChanged] will be called. Subsequently [Observer.onChanged] will also be called
+     * any time [value] changes. All of these callbacks are assumed to occur on the UI thread.
      */
     @UiThread
     public fun addObserver(observer: Observer<T>) {
@@ -98,7 +100,7 @@
 /**
  * [ObservableWatchData] which publicly exposes [setValue(T)] method.
  *
- * @param <T> The type of data hold by this instance
+ * @param T The type of data held by this instance
  */
 public class MutableObservableWatchData<T : Any>(initialValue: T?) :
     ObservableWatchData<T>(initialValue) {
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
index d102e50..46e38ef 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
@@ -16,6 +16,8 @@
 
 package androidx.wear.watchface
 
+import android.graphics.Color
+import androidx.annotation.ColorInt
 import androidx.annotation.RestrictTo
 import androidx.wear.watchface.data.RenderParametersWireFormat
 import androidx.wear.watchface.style.Layer
@@ -48,7 +50,8 @@
     DRAW,
 
     /**
-     * This layer should be rendered with highlighting (used by the editor). See also
+     * This layer should be rendered with highlighting (used by the editor) using
+     * [RenderParameters.highlightTint]. See also
      * [RenderParameters.highlightedComplicationId] for use in combination with
      * [Layer.COMPLICATIONS].
      */
@@ -77,7 +80,11 @@
      */
     @SuppressWarnings("AutoBoxing")
     @get:SuppressWarnings("AutoBoxing")
-    public val highlightedComplicationId: Int?
+    public val highlightedComplicationId: Int?,
+
+    /** Specifies the tint should be used for highlights. */
+    @ColorInt
+    public val highlightTint: Int
 ) {
     public companion object {
         /** A layerParameters map where all Layers have [LayerMode.DRAW]. */
@@ -88,7 +95,7 @@
         /** Default RenderParameters which draws everything in interactive mode. */
         @JvmField
         public val DEFAULT_INTERACTIVE: RenderParameters =
-            RenderParameters(DrawMode.INTERACTIVE, DRAW_ALL_LAYERS, null)
+            RenderParameters(DrawMode.INTERACTIVE, DRAW_ALL_LAYERS, null, Color.RED)
     }
 
     /** @hide */
@@ -99,7 +106,8 @@
             { Layer.values()[it.layer] },
             { LayerMode.values()[it.layerMode] }
         ),
-        wireFormat.highlightedComplicationId
+        wireFormat.highlightedComplicationId,
+        wireFormat.highlightTint
     )
 
     /** @hide */
@@ -112,6 +120,7 @@
                 it.value.ordinal
             )
         },
-        highlightedComplicationId
+        highlightedComplicationId,
+        highlightTint
     )
 }
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index 8a6ff58..9c62c0d 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -16,17 +16,79 @@
 
 package androidx.wear.watchface
 
+import android.annotation.SuppressLint
+import android.content.res.Configuration
 import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Rect
 import android.icu.util.Calendar
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import android.opengl.EGLDisplay
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.util.Log
 import android.view.SurfaceHolder
+import androidx.annotation.CallSuper
+import androidx.annotation.IntDef
 import androidx.annotation.IntRange
 import androidx.annotation.Px
 import androidx.annotation.UiThread
+import androidx.wear.watchface.Renderer.CanvasRenderer
+import androidx.wear.watchface.Renderer.GlesRenderer
 import androidx.wear.watchface.style.UserStyleRepository
+import java.nio.ByteBuffer
+
+/**
+ * Describes the type of [Canvas] a [CanvasRenderer] should request from a [SurfaceHolder].
+ *
+ * @hide
+ */
+@IntDef(
+    value = [
+        CanvasType.SOFTWARE,
+        CanvasType.HARDWARE
+    ]
+)
+public annotation class CanvasType {
+    public companion object {
+        /** A software canvas will be requested. */
+        public const val SOFTWARE: Int = 0
+
+        /**
+         * A hardware canvas will be requested. This is usually faster than software rendering,
+         * however it can sometimes increase battery usage by rendering at a higher frame rate.
+         *
+         * NOTE this is only supported on API level 26 and above. On lower API levels we fall back
+         * to a software canvas.
+         */
+        public const val HARDWARE: Int = 1
+    }
+}
+
+internal val EGL_CONFIG_ATTRIB_LIST = intArrayOf(
+    EGL14.EGL_RENDERABLE_TYPE,
+    EGL14.EGL_OPENGL_ES2_BIT,
+    EGL14.EGL_RED_SIZE,
+    8,
+    EGL14.EGL_GREEN_SIZE,
+    8,
+    EGL14.EGL_BLUE_SIZE,
+    8,
+    EGL14.EGL_ALPHA_SIZE,
+    8,
+    EGL14.EGL_NONE
+)
+
+private val EGL_CONTEXT_ATTRIB_LIST =
+    intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
+
+internal val EGL_SURFACE_ATTRIB_LIST = intArrayOf(EGL14.EGL_NONE)
 
 /** The base class for [CanvasRenderer] and [GlesRenderer]. */
-public abstract class Renderer(
+public sealed class Renderer(
     /** The [SurfaceHolder] that [renderInternal] will draw into. */
     public val surfaceHolder: SurfaceHolder,
 
@@ -74,8 +136,8 @@
     /**
      * The bounds of the [SurfaceHolder] this Renderer renders into. Depending on the shape of the
      * device's screen not all of these pixels may be visible to the user (see
-     * [WatchState.screenShape]).  Note also that API level 27+ devices draw indicators in the top
-     * and bottom 24dp of the screen, avoid rendering anything important there.
+     * [Configuration.isScreenRound]).  Note also that API level 27+ devices draw indicators in the
+     * top and bottom 24dp of the screen, avoid rendering anything important there.
      */
     public var screenBounds: Rect = surfaceHolder.surfaceFrame
         private set
@@ -135,8 +197,8 @@
     ): Bitmap
 
     /**
-     * Called when the [DrawMode] has been updated. Will always be called before the first
-     * call to onDraw().
+     * Called when the [RenderParameters] has been updated. Will always be called before the first
+     * call to [CanvasRenderer.render] or [GlesRenderer.render].
      */
     @UiThread
     protected open fun onRenderParametersChanged(renderParameters: RenderParameters) {
@@ -176,17 +238,400 @@
     public open fun shouldAnimate(): Boolean =
         watchState.isVisible.value && !watchState.isAmbient.value
 
-    /** Schedules a call to [renderInternal] to draw the next frame. */
+    /**
+     * Schedules a call to either [CanvasRenderer.render] or [GlesRenderer.render] to draw the next
+     * frame.
+     */
     @UiThread
     public fun invalidate() {
-        watchFaceHostApi.invalidate()
+        if (this::watchFaceHostApi.isInitialized) {
+            watchFaceHostApi.invalidate()
+        }
     }
 
     /**
-     * Posts a message to schedule a call to [renderInternal] to draw the next frame. Unlike
-     * [invalidate], this method is thread-safe and may be called on any thread.
+     * Posts a message to schedule a call to either [CanvasRenderer.render] or [GlesRenderer.render]
+     * to draw the next frame. Unlike [invalidate], this method is thread-safe and may be called
+     * on any thread.
      */
     public fun postInvalidate() {
-        watchFaceHostApi.getHandler().post { watchFaceHostApi.invalidate() }
+        if (this::watchFaceHostApi.isInitialized) {
+            watchFaceHostApi.getHandler().post { watchFaceHostApi.invalidate() }
+        }
+    }
+
+    /**
+     * Watch faces that require [Canvas] rendering should extend their [Renderer] from this class.
+     */
+    public abstract class CanvasRenderer(
+        /**
+         * The [SurfaceHolder] from which a [Canvas] to will be obtained and passed into [render].
+         */
+        surfaceHolder: SurfaceHolder,
+
+        /** The watch face's associated [UserStyleRepository]. */
+        userStyleRepository: UserStyleRepository,
+
+        /** The watch face's associated [WatchState]. */
+        watchState: WatchState,
+
+        /** The type of canvas to request. */
+        @CanvasType private val canvasType: Int,
+
+        /**
+         * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
+         * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces
+         * are recommended to use lower frame rates if possible for better battery life. Variable
+         * frame  rates can also help preserve battery life, e.g. if a watch face has a short
+         * animation once per second it can adjust the frame rate inorder to sleep when not
+         * animating.
+         */
+        @IntRange(from = 0, to = 10000)
+        interactiveDrawModeUpdateDelayMillis: Long
+    ) : Renderer(
+        surfaceHolder,
+        userStyleRepository,
+        watchState,
+        interactiveDrawModeUpdateDelayMillis
+    ) {
+
+        @SuppressWarnings("UnsafeNewApiCall") // We check if the SDK is new enough.
+        internal override fun renderInternal(
+            calendar: Calendar
+        ) {
+            val canvas = (
+                if (canvasType == CanvasType.HARDWARE && android.os.Build.VERSION.SDK_INT >= 26) {
+                    surfaceHolder.lockHardwareCanvas() // Requires API level 26.
+                } else {
+                    surfaceHolder.lockCanvas()
+                }
+                ) ?: return
+            try {
+                if (watchState.isVisible.value) {
+                    render(canvas, surfaceHolder.surfaceFrame, calendar)
+                } else {
+                    canvas.drawColor(Color.BLACK)
+                }
+            } finally {
+                surfaceHolder.unlockCanvasAndPost(canvas)
+            }
+        }
+
+        /** {@inheritDoc} */
+        internal override fun takeScreenshot(
+            calendar: Calendar,
+            renderParameters: RenderParameters
+        ): Bitmap {
+            val bitmap = Bitmap.createBitmap(
+                screenBounds.width(),
+                screenBounds.height(),
+                Bitmap.Config.ARGB_8888
+            )
+            val prevRenderParameters = this.renderParameters
+            this.renderParameters = renderParameters
+            render(Canvas(bitmap), screenBounds, calendar)
+            this.renderParameters = prevRenderParameters
+            return bitmap
+        }
+
+        /**
+         * Sub-classes should override this to implement their rendering logic which should respect
+         * the current [DrawMode]. For correct functioning the CanvasRenderer must use the supplied
+         * [Calendar] in favor of any other ways of getting the time.
+         *
+         * @param canvas The [Canvas] to render into. Don't assume this is always the canvas from
+         *     the [SurfaceHolder] backing the display
+         * @param bounds A [Rect] describing the bonds of the canvas to draw into
+         * @param calendar The current [Calendar]
+         */
+        @UiThread
+        public abstract fun render(
+            canvas: Canvas,
+            bounds: Rect,
+            calendar: Calendar
+        )
+    }
+
+    /**
+     * Watch faces that require [GLES20] rendering should extend their [Renderer] from this class.
+     */
+    public abstract class GlesRenderer @JvmOverloads constructor(
+        /** The [SurfaceHolder] whose [android.view.Surface] [render] will draw into. */
+        surfaceHolder: SurfaceHolder,
+
+        /** The associated [UserStyleRepository]. */
+        userStyleRepository: UserStyleRepository,
+
+        /** The associated [WatchState]. */
+        watchState: WatchState,
+
+        /**
+         * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
+         * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces
+         * are recommended to use lower frame rates if possible for better battery life. Variable
+         * frame rates can also help preserve battery life, e.g. if a watch face has a short
+         * animation once per second it can adjust the frame rate inorder to sleep when not
+         * animating.
+         */
+        @IntRange(from = 0, to = 10000)
+        interactiveDrawModeUpdateDelayMillis: Long,
+
+        /**
+         * Attributes for [EGL14.eglChooseConfig]. By default this selects an RGBA8888 back buffer.
+         */
+        private val eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
+
+        /**
+         * The attributes to be passed to [EGL14.eglCreateWindowSurface]. By default this is empty.
+         */
+        private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
+    ) : Renderer(
+        surfaceHolder,
+        userStyleRepository,
+        watchState,
+        interactiveDrawModeUpdateDelayMillis
+    ) {
+        /** @hide */
+        private companion object {
+            private const val TAG = "Gles2WatchFace"
+        }
+
+        private var eglDisplay: EGLDisplay? = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY).apply {
+            if (this == EGL14.EGL_NO_DISPLAY) {
+                throw RuntimeException("eglGetDisplay returned EGL_NO_DISPLAY")
+            }
+            // Initialize the display. The major and minor version numbers are passed back.
+            val version = IntArray(2)
+            if (!EGL14.eglInitialize(this, version, 0, version, 1)) {
+                throw RuntimeException("eglInitialize failed")
+            }
+        }
+
+        private var eglConfig: EGLConfig = chooseEglConfig(eglDisplay!!)
+
+        @SuppressWarnings("SyntheticAccessor")
+        private var eglContext: EGLContext? = EGL14.eglCreateContext(
+            eglDisplay,
+            eglConfig,
+            EGL14.EGL_NO_CONTEXT,
+            EGL_CONTEXT_ATTRIB_LIST,
+            0
+        )
+
+        init {
+            if (eglContext == EGL14.EGL_NO_CONTEXT) {
+                throw RuntimeException("eglCreateContext failed")
+            }
+        }
+
+        private var eglSurface: EGLSurface? = null
+        private var calledOnGlContextCreated = false
+
+        /**
+         * Chooses the EGLConfig to use.
+         * @throws RuntimeException if [EGL14.eglChooseConfig] fails
+         */
+        private fun chooseEglConfig(eglDisplay: EGLDisplay): EGLConfig {
+            val numEglConfigs = IntArray(1)
+            val eglConfigs = arrayOfNulls<EGLConfig>(1)
+            if (!EGL14.eglChooseConfig(
+                    eglDisplay,
+                    eglConfigAttribList,
+                    0,
+                    eglConfigs,
+                    0,
+                    eglConfigs.size,
+                    numEglConfigs,
+                    0
+                )
+            ) {
+                throw RuntimeException("eglChooseConfig failed")
+            }
+            if (numEglConfigs[0] == 0) {
+                throw RuntimeException("no matching EGL configs")
+            }
+            return eglConfigs[0]!!
+        }
+
+        private fun createWindowSurface(width: Int, height: Int) {
+            if (eglSurface != null) {
+                if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+                    Log.w(TAG, "eglDestroySurface failed")
+                }
+            }
+            eglSurface = EGL14.eglCreateWindowSurface(
+                eglDisplay,
+                eglConfig,
+                surfaceHolder.surface,
+                eglSurfaceAttribList,
+                0
+            )
+            if (eglSurface == EGL14.EGL_NO_SURFACE) {
+                throw RuntimeException("eglCreateWindowSurface failed")
+            }
+
+            makeContextCurrent()
+            GLES20.glViewport(0, 0, width, height)
+            if (!calledOnGlContextCreated) {
+                calledOnGlContextCreated = true
+                onGlContextCreated()
+            }
+            onGlSurfaceCreated(width, height)
+        }
+
+        @CallSuper
+        override fun onDestroy() {
+            if (eglSurface != null) {
+                if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+                    Log.w(TAG, "eglDestroySurface failed")
+                }
+                eglSurface = null
+            }
+            if (eglContext != null) {
+                if (!EGL14.eglDestroyContext(eglDisplay, eglContext)) {
+                    Log.w(TAG, "eglDestroyContext failed")
+                }
+                eglContext = null
+            }
+            if (eglDisplay != null) {
+                if (!EGL14.eglTerminate(eglDisplay)) {
+                    Log.w(TAG, "eglTerminate failed")
+                }
+                eglDisplay = null
+            }
+        }
+
+        /**
+         * Sets our GL context to be the current one. This method *must* be called before any OpenGL
+         * APIs are used.
+         */
+        private fun makeContextCurrent() {
+            if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
+                throw RuntimeException("eglMakeCurrent failed")
+            }
+        }
+
+        internal override fun onPostCreate() {
+            surfaceHolder.addCallback(object : SurfaceHolder.Callback {
+                @SuppressLint("SyntheticAccessor")
+                override fun surfaceChanged(
+                    holder: SurfaceHolder,
+                    format: Int,
+                    width: Int,
+                    height: Int
+                ) {
+                    createWindowSurface(width, height)
+                }
+
+                @SuppressLint("SyntheticAccessor")
+                override fun surfaceDestroyed(holder: SurfaceHolder) {
+                    if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+                        Log.w(TAG, "eglDestroySurface failed")
+                    }
+                    eglSurface = null
+                }
+
+                override fun surfaceCreated(holder: SurfaceHolder) {
+                }
+            })
+
+            createWindowSurface(
+                surfaceHolder.surfaceFrame.width(),
+                surfaceHolder.surfaceFrame.height()
+            )
+        }
+
+        /** Called when a new GL context is created. It's safe to use GL APIs in this method. */
+        @UiThread
+        public open fun onGlContextCreated() {
+        }
+
+        /**
+         * Called when a new GL surface is created. It's safe to use GL APIs in this method.
+         *
+         * @param width width of surface in pixels
+         * @param height height of surface in pixels
+         */
+        @UiThread
+        public open fun onGlSurfaceCreated(width: Int, height: Int) {
+        }
+
+        internal override fun renderInternal(
+            calendar: Calendar
+        ) {
+            makeContextCurrent()
+            render(calendar)
+            if (!EGL14.eglSwapBuffers(eglDisplay, eglSurface)) {
+                Log.w(TAG, "eglSwapBuffers failed")
+            }
+        }
+
+        /** {@inheritDoc} */
+        internal override fun takeScreenshot(
+            calendar: Calendar,
+            renderParameters: RenderParameters
+        ): Bitmap {
+            val width = screenBounds.width()
+            val height = screenBounds.height()
+            val pixelBuf = ByteBuffer.allocateDirect(width * height * 4)
+            makeContextCurrent()
+            val prevRenderParameters = this.renderParameters
+            this.renderParameters = renderParameters
+            render(calendar)
+            this.renderParameters = prevRenderParameters
+            GLES20.glFinish()
+            GLES20.glReadPixels(
+                0,
+                0,
+                width,
+                height,
+                GLES20.GL_RGBA,
+                GLES20.GL_UNSIGNED_BYTE,
+                pixelBuf
+            )
+            // The image is flipped when using read pixels because the first pixel in the OpenGL
+            // buffer is in bottom left.
+            verticalFlip(pixelBuf, width, height)
+            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+            bitmap.copyPixelsFromBuffer(pixelBuf)
+            return bitmap
+        }
+
+        private fun verticalFlip(
+            buffer: ByteBuffer,
+            width: Int,
+            height: Int
+        ) {
+            var i = 0
+            val tmp = ByteArray(width * 4)
+            while (i++ < height / 2) {
+                buffer[tmp]
+                System.arraycopy(
+                    buffer.array(),
+                    buffer.limit() - buffer.position(),
+                    buffer.array(),
+                    buffer.position() - width * 4,
+                    width * 4
+                )
+                System.arraycopy(
+                    tmp,
+                    0,
+                    buffer.array(),
+                    buffer.limit() - buffer.position(),
+                    width * 4
+                )
+            }
+            buffer.rewind()
+        }
+
+        /**
+         * Sub-classes should override this to implement their rendering logic which should respect
+         * the current [DrawMode]. For correct functioning the GlesRenderer must use the supplied
+         * [Calendar] in favor of any other ways of getting the time.
+         *
+         * @param calendar The current [Calendar]
+         */
+        @UiThread
+        public abstract fun render(calendar: Calendar)
     }
 }
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index cdb2fff..1feaac7 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -18,11 +18,10 @@
 
 import android.annotation.SuppressLint
 import android.app.NotificationManager
-import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
+import android.graphics.Color
 import android.graphics.Point
 import android.graphics.Rect
 import android.icu.util.Calendar
@@ -39,7 +38,6 @@
 import androidx.annotation.VisibleForTesting
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationData
-import androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle
 import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
 import androidx.wear.watchface.data.RenderParametersWireFormat
 import androidx.wear.watchface.style.UserStyle
@@ -105,8 +103,8 @@
 }
 
 /**
- * A WatchFace is constructed by a user's [WatchFaceService] and brings together rendering,
- * styling, complications and state observers.
+ * The return value of [WatchFaceService.createWatchFace] which brings together rendering, styling,
+ * complications and state observers.
  */
 public class WatchFace(
     /**
@@ -115,13 +113,13 @@
      */
     @WatchFaceType internal var watchFaceType: Int,
 
-    /** The {@UserStyleRepository} for this WatchFaceImpl. */
+    /** The [UserStyleRepository] for this WatchFace. */
     internal val userStyleRepository: UserStyleRepository,
 
-    /** The [ComplicationsManager] for this WatchFaceImpl. */
+    /** The [ComplicationsManager] for this WatchFace. */
     internal var complicationsManager: ComplicationsManager,
 
-    /** The [Renderer] for this WatchFaceImpl. */
+    /** The [Renderer] for this WatchFace. */
     internal val renderer: Renderer
 ) {
     internal var tapListener: TapListener? = null
@@ -210,7 +208,7 @@
         }
     }
 
-    /** The preview time in milliseconds since the epoch, or null if not set. */
+    /** The UTC preview time in milliseconds since the epoch, or null if not set. */
     @get:SuppressWarnings("AutoBoxing")
     @IntRange(from = 0)
     public var overridePreviewReferenceTimeMillis: Long? = null
@@ -331,39 +329,10 @@
     private val pendingPostDoubleTap: CancellableUniqueTask =
         CancellableUniqueTask(watchFaceHostApi.getHandler())
 
-    private val timeZoneReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            calendar.timeZone = TimeZone.getDefault()
-            watchFaceHostApi.invalidate()
-        }
-    }
-
-    private val timeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context?, intent: Intent?) {
-            // System time has changed hence next scheduled draw is invalid.
-            nextDrawTimeMillis = systemTimeProvider.getSystemTimeMillis()
-            watchFaceHostApi.invalidate()
-        }
-    }
-
-    internal val batteryLevelReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-        @SuppressWarnings("SyntheticAccessor")
-        override fun onReceive(context: Context, intent: Intent) {
-            val isBatteryLowAndNotCharging =
-                watchState.isBatteryLowAndNotCharging as MutableObservableWatchData
-            when (intent.action) {
-                Intent.ACTION_BATTERY_LOW -> isBatteryLowAndNotCharging.value = true
-                Intent.ACTION_BATTERY_OKAY -> isBatteryLowAndNotCharging.value = false
-                Intent.ACTION_POWER_CONNECTED -> isBatteryLowAndNotCharging.value = false
-            }
-            watchFaceHostApi.invalidate()
-        }
-    }
-
     private val componentName =
         ComponentName(
             watchFaceHostApi.getContext().packageName,
-            watchFaceHostApi.getContext().javaClass.typeName
+            watchFaceHostApi.getContext().javaClass.name
         )
 
     internal fun getWatchFaceStyle() = WatchFaceStyle(
@@ -376,14 +345,36 @@
         legacyWatchFaceStyle.tapEventsAccepted
     )
 
-    /**
-     * We listen for MOCK_TIME_INTENTs which we interpret as a request to modify time. E.g. speeding
-     * up or slowing down time, and providing support for making time loop between two instants.
-     * This is intended to help implement animations which may occur infrequently (e.g. hourly).
-     */
-    internal val mockTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-        @SuppressWarnings("SyntheticAccessor")
-        override fun onReceive(context: Context, intent: Intent) {
+    private val broadcastEventObserver = object : BroadcastReceivers.BroadcastEventObserver {
+        override fun onActionTimeTick() {
+            if (!watchState.isAmbient.value) {
+                renderer.invalidate()
+            }
+        }
+
+        override fun onActionTimeZoneChanged() {
+            calendar.timeZone = TimeZone.getDefault()
+            renderer.invalidate()
+        }
+
+        override fun onActionTimeChanged() {
+            // System time has changed hence next scheduled draw is invalid.
+            nextDrawTimeMillis = systemTimeProvider.getSystemTimeMillis()
+            renderer.invalidate()
+        }
+
+        override fun onActionBatteryChanged(intent: Intent) {
+            val isBatteryLowAndNotCharging =
+                watchState.isBatteryLowAndNotCharging as MutableObservableWatchData
+            when (intent.action) {
+                Intent.ACTION_BATTERY_LOW -> isBatteryLowAndNotCharging.value = true
+                Intent.ACTION_BATTERY_OKAY -> isBatteryLowAndNotCharging.value = false
+                Intent.ACTION_POWER_CONNECTED -> isBatteryLowAndNotCharging.value = false
+            }
+            renderer.invalidate()
+        }
+
+        override fun onMockTime(intent: Intent) {
             mockTime.speed = intent.getFloatExtra(
                 EXTRA_MOCK_TIME_SPEED_MULTIPLIER,
                 MOCK_TIME_DEFAULT_SPEED_MULTIPLIER
@@ -419,7 +410,7 @@
         } else {
             // The system doesn't support preference persistence we need to do it ourselves.
             val preferencesFile =
-                "watchface_prefs_${watchFaceHostApi.getContext().javaClass.typeName}.txt"
+                "watchface_prefs_${watchFaceHostApi.getContext().javaClass.name}.txt"
 
             userStyleRepository.userStyle = UserStyle(
                 readPrefs(watchFaceHostApi.getContext(), preferencesFile),
@@ -561,21 +552,9 @@
             return
         }
         registeredReceivers = true
-        watchFaceHostApi.getContext().registerReceiver(
-            timeZoneReceiver,
-            IntentFilter(Intent.ACTION_TIMEZONE_CHANGED)
-        )
-        watchFaceHostApi.getContext().registerReceiver(
-            timeReceiver,
-            IntentFilter(Intent.ACTION_TIME_CHANGED)
-        )
-        watchFaceHostApi.getContext().registerReceiver(
-            batteryLevelReceiver,
-            IntentFilter(Intent.ACTION_BATTERY_CHANGED)
-        )
-        watchFaceHostApi.getContext().registerReceiver(
-            mockTimeReceiver,
-            IntentFilter(MOCK_TIME_INTENT)
+        BroadcastReceivers.addBroadcastEventObserver(
+            watchFaceHostApi.getContext(),
+            broadcastEventObserver
         )
     }
 
@@ -584,10 +563,7 @@
             return
         }
         registeredReceivers = false
-        watchFaceHostApi.getContext().unregisterReceiver(timeZoneReceiver)
-        watchFaceHostApi.getContext().unregisterReceiver(timeReceiver)
-        watchFaceHostApi.getContext().unregisterReceiver(batteryLevelReceiver)
-        watchFaceHostApi.getContext().unregisterReceiver(mockTimeReceiver)
+        BroadcastReceivers.removeBroadcastEventObserver(broadcastEventObserver)
     }
 
     private fun scheduleDraw() {
@@ -637,7 +613,12 @@
             newDrawMode = DrawMode.MUTE
         }
         renderer.renderParameters =
-            RenderParameters(newDrawMode, RenderParameters.DRAW_ALL_LAYERS, null)
+            RenderParameters(
+                newDrawMode,
+                RenderParameters.DRAW_ALL_LAYERS,
+                null,
+                Color.BLACK // Required by the constructor but unused.
+            )
     }
 
     /** @hide */
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 4b41029..da8e3e7 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -17,23 +17,21 @@
 package androidx.wear.watchface
 
 import android.annotation.SuppressLint
-import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.graphics.Bitmap
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.icu.util.Calendar
 import android.icu.util.TimeZone
+import android.os.Build
 import android.os.Bundle
 import android.os.Handler
 import android.os.Looper
 import android.os.PowerManager
 import android.os.RemoteException
 import android.os.Trace
-import android.os.UserManager
 import android.service.wallpaper.WallpaperService
 import android.support.wearable.watchface.Constants
 import android.support.wearable.watchface.IWatchFaceService
@@ -121,22 +119,19 @@
 /**
  * WatchFaceService and [WatchFace] are a pair of classes intended to handle much of
  * the boilerplate needed to implement a watch face without being too opinionated. The suggested
- * structure of an WatchFaceService based watch face is:
+ * structure of a WatchFaceService based watch face is:
  *
  * @sample androidx.wear.watchface.samples.kDocCreateExampleWatchFaceService
  *
- * Base classes for complications and styles are provided along with a default UI for configuring
- * them. Complications are optional, however if required, WatchFaceService assumes all
- * complications can be enumerated up front and passed as a collection into [ComplicationsManager]'s
- * constructor which is passed to [WatchFace]'s constructor. Some watch faces support different
- * configurations (number & position) of complications and this can be achieved by rendering a
- * subset and only marking the ones you need as enabled (see
- * [UserStyleSetting.ComplicationsUserStyleSetting]).
+ * Sub classes of WatchFaceService are expected to implement [createWatchFace] which is the
+ * factory for making [WatchFace]s. All [Complication]s are assumed to be enumerated up upfront and
+ * passed as a collection into [ComplicationsManager]'s constructor which is in turn passed to
+ * [WatchFace]'s constructor. Complications can be enabled and disabled via [UserStyleSetting
+ * .ComplicationsUserStyleSetting].
  *
- * Many watch faces support styles, typically controlling the color and visual look of watch face
- * elements such as numeric fonts, watch hands and ticks. WatchFaceService doesn't take an
- * an opinion on what comprises a style beyond it should be representable as a map of categories to
- * options.
+ * Watch face styling (color and visual look of watch face elements such as numeric fonts, watch
+ * hands and ticks, etc...) is directly supported via [UserStyleSetting] and
+ * [UserStyleRepository].
  *
  * To aid debugging watch face animations, WatchFaceService allows you to speed up or slow down
  * time, and to loop between two instants.  This is controlled by MOCK_TIME_INTENT intents
@@ -199,7 +194,6 @@
  *
  * Multiple watch faces can be defined in the same package, requiring multiple <service> tags.
  */
-@RequiresApi(26)
 public abstract class WatchFaceService : WallpaperService() {
 
     /** @hide */
@@ -241,8 +235,9 @@
     // This is open for use by tests.
     internal open fun allowWatchFaceToAnimate() = true
 
+    // Whether or not the pre R style init flow (SET_BINDER wallpaper command) is expected.
     // This is open for use by tests.
-    internal open fun isUserUnlocked() = getSystemService(UserManager::class.java).isUserUnlocked
+    internal open fun expectPreRInitFlow() = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
 
     /**
      * This is open for use by tests, it allows them to inject a custom [SurfaceHolder].
@@ -267,14 +262,6 @@
             isVisible.value = this@EngineWrapper.isVisible
         }
 
-        private var timeTickRegistered = false
-        private val timeTickReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-            @SuppressWarnings("SyntheticAccessor")
-            override fun onReceive(context: Context, intent: Intent) {
-                watchFaceImpl.renderer.invalidate()
-            }
-        }
-
         /**
          * Whether or not we allow watchfaces to animate. In some tests or for headless
          * rendering (for remote config) we don't want this.
@@ -307,16 +294,6 @@
 
         private val invalidateRunnable = Runnable(this::invalidate)
 
-        private val ambientTimeTickFilter = IntentFilter().apply {
-            addAction(Intent.ACTION_DATE_CHANGED)
-            addAction(Intent.ACTION_TIME_CHANGED)
-            addAction(Intent.ACTION_TIMEZONE_CHANGED)
-        }
-
-        private val interactiveTimeTickFilter = IntentFilter(ambientTimeTickFilter).apply {
-            addAction(Intent.ACTION_TIME_TICK)
-        }
-
         // TODO(alexclarke): Figure out if we can remove this.
         private var pendingBackgroundAction: Bundle? = null
         private var pendingProperties: Bundle? = null
@@ -349,7 +326,7 @@
                 InteractiveInstanceManager.takePendingWallpaperInteractiveWatchFaceInstance()
 
             // In a direct boot scenario attempt to load the previously serialized parameters.
-            if (pendingWallpaperInstance == null && !isUserUnlocked()) {
+            if (pendingWallpaperInstance == null && !expectPreRInitFlow()) {
                 val params = readDirectBootPrefs(_context, DIRECT_BOOT_PREFS)
                 if (params != null) {
                     createInteractiveInstance(params).createWCSApi()
@@ -403,7 +380,6 @@
                 systemState.inAmbientMode != mutableWatchState.isAmbient.value
             ) {
                 mutableWatchState.isAmbient.value = systemState.inAmbientMode
-                updateTimeTickReceiver()
             }
 
             if (firstSetSystemState ||
@@ -462,7 +438,10 @@
                             it.value.defaultProviderPolicy.providersAsList(),
                             it.value.defaultProviderPolicy.systemProviderFallback,
                             it.value.defaultProviderType.asWireComplicationType(),
-                            it.value.enabled
+                            it.value.enabled,
+                            it.value.renderer.idAndData?.complicationData?.type
+                                ?.asWireComplicationType()
+                                ?: ComplicationType.NO_DATA.asWireComplicationType()
                         )
                     )
                 }
@@ -664,11 +643,6 @@
                 choreographer.removeFrameCallback(frameCallback)
             }
 
-            if (timeTickRegistered) {
-                timeTickRegistered = false
-                unregisterReceiver(timeTickReceiver)
-            }
-
             if (this::watchFaceImpl.isInitialized) {
                 watchFaceImpl.onDestroy()
             }
@@ -928,41 +902,6 @@
             pendingSetWatchFaceStyle = false
         }
 
-        /**
-         * Registers [timeTickReceiver] if it should be registered and isn't currently, or
-         * unregisters it if it shouldn't be registered but currently is. It also applies the right
-         * intent filter depending on whether we are in ambient mode or not.
-         */
-        internal fun updateTimeTickReceiver() {
-            // Separate calls are issued to deliver the state of isAmbient and isVisible, so during
-            // init we might not yet know the state of both.
-            if (!mutableWatchState.isAmbient.hasValue() ||
-                !mutableWatchState.isVisible.hasValue()
-            ) {
-                return
-            }
-
-            if (timeTickRegistered) {
-                unregisterReceiver(timeTickReceiver)
-                timeTickRegistered = false
-            }
-
-            // We only register if we are visible, otherwise it doesn't make sense to waste cycles.
-            if (mutableWatchState.isVisible.value) {
-                if (mutableWatchState.isAmbient.value) {
-                    registerReceiver(timeTickReceiver, ambientTimeTickFilter)
-                } else {
-                    registerReceiver(timeTickReceiver, interactiveTimeTickFilter)
-                }
-                timeTickRegistered = true
-
-                // In case we missed a tick while transitioning from ambient to interactive, we
-                // want to make sure the watch face doesn't show stale time when in interactive
-                // mode.
-                watchFaceImpl.renderer.invalidate()
-            }
-        }
-
         override fun onVisibilityChanged(visible: Boolean) {
             super.onVisibilityChanged(visible)
 
@@ -983,7 +922,6 @@
             }
 
             mutableWatchState.isVisible.value = visible
-            updateTimeTickReceiver()
             pendingVisibilityChanged = null
         }
 
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
index f9888b2..26df645 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
@@ -19,25 +19,10 @@
 import android.app.NotificationManager
 import androidx.annotation.RestrictTo
 
-import androidx.annotation.IntDef
-
-/** @hide */
-@IntDef(
-    value = [
-        ScreenShape.ROUND,
-        ScreenShape.RECTANGULAR
-    ]
-)
-public annotation class ScreenShape {
-    public companion object {
-        /** The watch screen has a circular shape. */
-        public const val ROUND: Int = 1
-
-        /** The watch screen has a rectangular or square shape. */
-        public const val RECTANGULAR: Int = 2
-    }
-}
-
+/**
+ * Describes the current state of the wearable including some hardware details such as whether or
+ * not it supports burn in prevention and low-bit ambient.
+ */
 public class WatchState(
     /**
      * The current user interruption settings. See [NotificationManager]. Based on the value
@@ -79,10 +64,6 @@
     @get:JvmName("hasBurnInProtection")
     public val hasBurnInProtection: Boolean,
 
-    /** The physical shape of the screen. */
-    @ScreenShape
-    public val screenShape: Int,
-
     /** UTC reference time for previews of analog watch faces in milliseconds since the epoch. */
     public val analogPreviewReferenceTimeMillis: Long,
 
@@ -100,8 +81,6 @@
     public val isVisible: MutableObservableWatchData<Boolean> = MutableObservableWatchData()
     public var hasLowBitAmbient: Boolean = false
     public var hasBurnInProtection: Boolean = false
-    @ScreenShape
-    public var screenShape: Int = 0
     public var analogPreviewReferenceTimeMillis: Long = 0
     public var digitalPreviewReferenceTimeMillis: Long = 0
 
@@ -112,7 +91,6 @@
         isVisible = isVisible,
         hasLowBitAmbient = hasLowBitAmbient,
         hasBurnInProtection = hasBurnInProtection,
-        screenShape = screenShape,
         analogPreviewReferenceTimeMillis = analogPreviewReferenceTimeMillis,
         digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis
     )
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index e9666da..28a81c3 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -115,4 +115,10 @@
         uiThreadHandler.runOnHandler {
             engine.watchFaceImpl.userStyleRepository.schema.toWireFormat()
         }
+
+    override fun bringAttentionToComplication(id: Int) {
+        uiThreadHandler.runOnHandler {
+            engine.watchFaceImpl.complicationsManager.bringAttentionToComplication(id)
+        }
+    }
 }
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index cd68cc9..c732a7c 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -31,9 +31,9 @@
 import androidx.wear.watchface.runOnHandler
 
 /**
- *  A service for creating and controlling WatchFaceInstances.
+ * A service for creating and controlling watch face instances.
  *
- *  @hide
+ * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @RequiresApi(27)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt
index b8b0dec..2065aee 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.Intent
 import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Rect
 import android.icu.util.Calendar
 import android.os.Bundle
@@ -196,7 +197,8 @@
                     Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
                     Layer.TOP_LAYER to LayerMode.DRAW
                 ),
-                null
+                null,
+                Color.RED
             ).toWireFormat()
         )
         canvas.drawBitmap(bitmap, drawRect, drawRect, null)
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index e8a1f0d..7b3158c 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -106,6 +106,10 @@
     override fun getHandler() = handler
 
     override fun getMutableWatchState() = watchState
+
+    fun setIsVisible(isVisible: Boolean) {
+        watchState.isVisible.value = isVisible
+    }
 }
 
 /**
@@ -169,7 +173,7 @@
     userStyleRepository: UserStyleRepository,
     watchState: WatchState,
     interactiveFrameRateMs: Long
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
     surfaceHolder,
     userStyleRepository,
     watchState,
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 1ad9e17..034a2c6 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -34,6 +34,7 @@
 import android.view.SurfaceHolder
 import android.view.ViewConfiguration
 import androidx.test.core.app.ApplicationProvider
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
@@ -169,7 +170,7 @@
                 ComplicationType.SMALL_IMAGE
             ),
             DefaultComplicationProviderPolicy(SystemProviders.SUNRISE_SUNSET),
-            RectF(0.2f, 0.4f, 0.4f, 0.6f)
+            ComplicationBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
 
@@ -190,7 +191,7 @@
                 ComplicationType.SMALL_IMAGE
             ),
             DefaultComplicationProviderPolicy(SystemProviders.DAY_OF_WEEK),
-            RectF(0.6f, 0.4f, 0.8f, 0.6f)
+            ComplicationBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
 
@@ -328,6 +329,7 @@
         sendImmutableProperties(engineWrapper, hasLowBitAmbient, hasBurnInProtection)
 
         watchFaceImpl = engineWrapper.watchFaceImpl
+        testWatchFaceService.setIsVisible(true)
     }
 
     private fun initWallpaperInteractiveWatchFaceInstance(
@@ -377,6 +379,7 @@
         // The [SurfaceHolder] must be sent before binding.
         engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
         watchFaceImpl = engineWrapper.watchFaceImpl
+        testWatchFaceService.setIsVisible(true)
     }
 
     private fun sendBinder(engine: WatchFaceService.EngineWrapper, apiVersion: Int) {
@@ -517,8 +520,7 @@
         watchState.isAmbient.value = false
         testWatchFaceService.mockSystemTimeMillis = 1000L
 
-        watchFaceImpl.mockTimeReceiver.onReceive(
-            context,
+        BroadcastReceivers.sendOnMockTimeForTesting(
             Intent(WatchFaceImpl.MOCK_TIME_INTENT).apply {
                 putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_SPEED_MULTIPLIER, 2.0f)
                 putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_WRAPPING_MIN_TIME, -1L)
@@ -545,8 +547,7 @@
         watchState.isAmbient.value = false
         testWatchFaceService.mockSystemTimeMillis = 1000L
 
-        watchFaceImpl.mockTimeReceiver.onReceive(
-            context,
+        BroadcastReceivers.sendOnMockTimeForTesting(
             Intent(WatchFaceImpl.MOCK_TIME_INTENT).apply {
                 putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_SPEED_MULTIPLIER, 2.0f)
                 putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_WRAPPING_MIN_TIME, 1000L)
@@ -874,19 +875,13 @@
         )
 
         // The delay should change when battery is low.
-        watchFaceImpl.batteryLevelReceiver.onReceive(
-            context,
-            Intent(Intent.ACTION_BATTERY_LOW)
-        )
+        BroadcastReceivers.sendOnActionBatteryChangedForTesting(Intent(Intent.ACTION_BATTERY_LOW))
         assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
             WatchFaceImpl.MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS
         )
 
         // And go back to normal when battery is OK.
-        watchFaceImpl.batteryLevelReceiver.onReceive(
-            context,
-            Intent(Intent.ACTION_BATTERY_OKAY)
-        )
+        BroadcastReceivers.sendOnActionBatteryChangedForTesting(Intent(Intent.ACTION_BATTERY_OKAY))
         assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
             INTERACTIVE_UPDATE_RATE_MS
         )
@@ -1261,8 +1256,8 @@
             4
         )
 
-        leftComplication.unitSquareBounds = RectF(0.3f, 0.3f, 0.5f, 0.5f)
-        rightComplication.unitSquareBounds = RectF(0.7f, 0.75f, 0.9f, 0.95f)
+        leftComplication.complicationBounds = ComplicationBounds(RectF(0.3f, 0.3f, 0.5f, 0.5f))
+        rightComplication.complicationBounds = ComplicationBounds(RectF(0.7f, 0.75f, 0.9f, 0.95f))
 
         val complicationDetails = engineWrapper.getComplicationState()
         assertThat(complicationDetails[0].id).isEqualTo(LEFT_COMPLICATION_ID)
@@ -1403,7 +1398,7 @@
                 provider2,
                 SystemProviders.SUNRISE_SUNSET
             ),
-            RectF(0.2f, 0.4f, 0.4f, 0.6f)
+            ComplicationBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
         initEngine(WatchFaceType.ANALOG, listOf(complication), UserStyleSchema(emptyList()))
@@ -1431,7 +1426,7 @@
                 provider2,
                 SystemProviders.SUNRISE_SUNSET
             ),
-            RectF(0.2f, 0.4f, 0.4f, 0.6f)
+            ComplicationBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
         initEngine(
@@ -1852,4 +1847,17 @@
         assertThat(leftComplication.isActiveAt(2000000)).isTrue()
         assertThat(leftComplication.isActiveAt(2000001)).isFalse()
     }
+
+    @Test
+    fun invalidateRendererBeforeFullInit() {
+        renderer = TestRenderer(
+            surfaceHolder,
+            UserStyleRepository(UserStyleSchema(emptyList())),
+            watchState.asWatchState(),
+            INTERACTIVE_UPDATE_RATE_MS
+        )
+
+        // This should not throw an exception.
+        renderer.invalidate()
+    }
 }
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
index 685668c..fd91bb2 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
@@ -18,20 +18,21 @@
 
 import android.content.ComponentName
 import android.content.Context
-import android.graphics.Bitmap
+import android.graphics.Canvas
 import android.graphics.Rect
 import android.graphics.RectF
 import android.icu.util.Calendar
 import android.view.SurfaceHolder
 import androidx.test.core.app.ApplicationProvider
+import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.watchface.CanvasComplicationDrawable
+import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.MutableWatchState
-import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFaceTestRunner
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
@@ -135,7 +136,7 @@
                 ComplicationType.SMALL_IMAGE
             ),
             DefaultComplicationProviderPolicy(SystemProviders.SUNRISE_SUNSET),
-            RectF(0.2f, 0.4f, 0.4f, 0.6f)
+            ComplicationBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
 
@@ -156,7 +157,7 @@
                 ComplicationType.SMALL_IMAGE
             ),
             DefaultComplicationProviderPolicy(SystemProviders.DAY_OF_WEEK),
-            RectF(0.6f, 0.4f, 0.8f, 0.6f)
+            ComplicationBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
         ).setDefaultProviderType(ComplicationType.SHORT_TEXT)
             .build()
 
@@ -196,20 +197,14 @@
         val complicationSet = ComplicationsManager(
             complications,
             userStyleRepository,
-            object : Renderer(
+            object : Renderer.CanvasRenderer(
                 surfaceHolder,
                 userStyleRepository,
                 watchState.asWatchState(),
+                CanvasType.SOFTWARE,
                 INTERACTIVE_UPDATE_RATE_MS
             ) {
-                override fun renderInternal(calendar: Calendar) {}
-
-                override fun takeScreenshot(
-                    calendar: Calendar,
-                    renderParameters: RenderParameters
-                ): Bitmap {
-                    throw RuntimeException("Not Implemented!")
-                }
+                override fun render(canvas: Canvas, bounds: Rect, calendar: Calendar) {}
             }
         )
 
diff --git a/wear/wear/api/api_lint.ignore b/wear/wear/api/api_lint.ignore
index aedd625..e513a66 100644
--- a/wear/wear/api/api_lint.ignore
+++ b/wear/wear/api/api_lint.ignore
@@ -25,24 +25,6 @@
     Public class androidx.wear.widget.SwipeDismissController stripped of unavailable superclass androidx.wear.widget.DismissController
 
 
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setAnimatedIcon(android.graphics.drawable.Icon):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getAnimatedIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setAnimatedIcon(android.graphics.drawable.Icon)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setAnimatedIcon(int):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getAnimatedIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setAnimatedIcon(int)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setStaticIcon(android.graphics.drawable.Icon):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getStaticIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setStaticIcon(android.graphics.drawable.Icon)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setStaticIcon(int):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getStaticIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setStaticIcon(int)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setStatus(androidx.wear.ongoingactivity.OngoingActivityStatus):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getStatus()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setStatus(androidx.wear.ongoingactivity.OngoingActivityStatus)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setTouchIntent(android.app.PendingIntent):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getTouchIntent()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setTouchIntent(android.app.PendingIntent)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setLocusId(androidx.core.content.LocusIdCompat):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getLocusId()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setLocusId(androidx.core.content.LocusIdCompat)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setOngoingActivityId(int):
-    androidx.wear.ongoingactivity.OngoingActivity does not declare a `getOngoingActivityId()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setOngoingActivityId(int)
-
-
 MissingNullability: androidx.wear.activity.ConfirmationActivity#onCreate(android.os.Bundle) parameter #0:
     Missing nullability on parameter `savedInstanceState` in method `onCreate`
 MissingNullability: androidx.wear.ambient.AmbientMode#attachAmbientSupport(T):
diff --git a/wear/wear/api/current.txt b/wear/wear/api/current.txt
index 2e3073a..363b0a2 100644
--- a/wear/wear/api/current.txt
+++ b/wear/wear/api/current.txt
@@ -73,6 +73,7 @@
   public final class AmbientModeSupport.AmbientController {
     method public boolean isAmbient();
     method public void setAmbientOffloadEnabled(boolean);
+    method public void setAutoResumeEnabled(boolean);
   }
 
 }
@@ -92,6 +93,7 @@
     method public androidx.wear.ongoingactivity.OngoingActivity build();
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+    method public androidx.wear.ongoingactivity.OngoingActivity.Builder setCategory(String);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setOngoingActivityId(int);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
@@ -103,24 +105,23 @@
   public class OngoingActivityData implements androidx.versionedparcelable.VersionedParcelable {
     method public static androidx.wear.ongoingactivity.OngoingActivityData? create(android.app.Notification);
     method public android.graphics.drawable.Icon? getAnimatedIcon();
+    method public String? getCategory();
     method public androidx.core.content.LocusIdCompat? getLocusId();
     method public int getOngoingActivityId();
     method public android.graphics.drawable.Icon getStaticIcon();
     method public androidx.wear.ongoingactivity.OngoingActivityStatus? getStatus();
+    method public long getTimestamp();
     method public android.app.PendingIntent getTouchIntent();
     method public static boolean hasOngoingActivity(android.app.Notification);
   }
 
-  public abstract class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
-    ctor public OngoingActivityStatus();
-    method public abstract long getNextChangeTimeMillis(long);
-    method public abstract CharSequence getText(android.content.Context, long);
+  public class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
+    method public long getNextChangeTimeMillis(long);
+    method public CharSequence getText(android.content.Context, long);
   }
 
   public class TextOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
     ctor public TextOngoingActivityStatus(String);
-    method public long getNextChangeTimeMillis(long);
-    method public CharSequence getText(android.content.Context, long);
   }
 
   public class TimerOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
@@ -128,9 +129,7 @@
     ctor public TimerOngoingActivityStatus(long, boolean, long);
     ctor public TimerOngoingActivityStatus(long, boolean);
     ctor public TimerOngoingActivityStatus(long);
-    method public long getNextChangeTimeMillis(long);
     method public long getPausedAtMillis();
-    method public CharSequence getText(android.content.Context, long);
     method public long getTimeZeroMillis();
     method public long getTotalDurationMillis();
     method public boolean hasTotalDuration();
@@ -317,14 +316,13 @@
   }
 
   public static interface WearArcLayout.ArcLayoutWidget {
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getSweepAngleDegrees();
     method public int getThicknessPx();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
   }
 
-  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
     ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
     ctor public WearArcLayout.LayoutParams(int, int);
     ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -342,7 +340,7 @@
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getAnchorAngleDegrees();
     method public int getAnchorType();
     method public boolean getClockwise();
@@ -358,7 +356,6 @@
     method public float getTextSize();
     method public int getThicknessPx();
     method public android.graphics.Typeface? getTypeface();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
     method public void setAnchorAngleDegrees(float);
     method public void setAnchorType(int);
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index 13c916ff..22878f3 100644
--- a/wear/wear/api/public_plus_experimental_current.txt
+++ b/wear/wear/api/public_plus_experimental_current.txt
@@ -73,6 +73,7 @@
   public final class AmbientModeSupport.AmbientController {
     method public boolean isAmbient();
     method public void setAmbientOffloadEnabled(boolean);
+    method public void setAutoResumeEnabled(boolean);
   }
 
 }
@@ -92,6 +93,7 @@
     method public androidx.wear.ongoingactivity.OngoingActivity build();
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+    method public androidx.wear.ongoingactivity.OngoingActivity.Builder setCategory(String);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setOngoingActivityId(int);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
@@ -103,24 +105,23 @@
   @androidx.versionedparcelable.VersionedParcelize public class OngoingActivityData implements androidx.versionedparcelable.VersionedParcelable {
     method public static androidx.wear.ongoingactivity.OngoingActivityData? create(android.app.Notification);
     method public android.graphics.drawable.Icon? getAnimatedIcon();
+    method public String? getCategory();
     method public androidx.core.content.LocusIdCompat? getLocusId();
     method public int getOngoingActivityId();
     method public android.graphics.drawable.Icon getStaticIcon();
     method public androidx.wear.ongoingactivity.OngoingActivityStatus? getStatus();
+    method public long getTimestamp();
     method public android.app.PendingIntent getTouchIntent();
     method public static boolean hasOngoingActivity(android.app.Notification);
   }
 
-  @androidx.versionedparcelable.VersionedParcelize public abstract class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
-    ctor public OngoingActivityStatus();
-    method public abstract long getNextChangeTimeMillis(long);
-    method public abstract CharSequence getText(android.content.Context, long);
+  @androidx.versionedparcelable.VersionedParcelize public class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
+    method public long getNextChangeTimeMillis(long);
+    method public CharSequence getText(android.content.Context, long);
   }
 
   @androidx.versionedparcelable.VersionedParcelize public class TextOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
     ctor public TextOngoingActivityStatus(String);
-    method public long getNextChangeTimeMillis(long);
-    method public CharSequence getText(android.content.Context, long);
   }
 
   @androidx.versionedparcelable.VersionedParcelize public class TimerOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
@@ -128,9 +129,7 @@
     ctor public TimerOngoingActivityStatus(long, boolean, long);
     ctor public TimerOngoingActivityStatus(long, boolean);
     ctor public TimerOngoingActivityStatus(long);
-    method public long getNextChangeTimeMillis(long);
     method public long getPausedAtMillis();
-    method public CharSequence getText(android.content.Context, long);
     method public long getTimeZeroMillis();
     method public long getTotalDurationMillis();
     method public boolean hasTotalDuration();
@@ -317,14 +316,13 @@
   }
 
   public static interface WearArcLayout.ArcLayoutWidget {
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getSweepAngleDegrees();
     method public int getThicknessPx();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
   }
 
-  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
     ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
     ctor public WearArcLayout.LayoutParams(int, int);
     ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -342,7 +340,7 @@
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getAnchorAngleDegrees();
     method public int getAnchorType();
     method public boolean getClockwise();
@@ -358,7 +356,6 @@
     method public float getTextSize();
     method public int getThicknessPx();
     method public android.graphics.Typeface? getTypeface();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
     method public void setAnchorAngleDegrees(float);
     method public void setAnchorType(int);
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 07cb190..9be0044 100644
--- a/wear/wear/api/restricted_current.txt
+++ b/wear/wear/api/restricted_current.txt
@@ -73,6 +73,7 @@
   public final class AmbientModeSupport.AmbientController {
     method public boolean isAmbient();
     method public void setAmbientOffloadEnabled(boolean);
+    method public void setAutoResumeEnabled(boolean);
   }
 
 }
@@ -92,6 +93,7 @@
     method public androidx.wear.ongoingactivity.OngoingActivity build();
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+    method public androidx.wear.ongoingactivity.OngoingActivity.Builder setCategory(String);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setOngoingActivityId(int);
     method public androidx.wear.ongoingactivity.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
@@ -103,24 +105,23 @@
   @androidx.versionedparcelable.VersionedParcelize public class OngoingActivityData implements androidx.versionedparcelable.VersionedParcelable {
     method public static androidx.wear.ongoingactivity.OngoingActivityData? create(android.app.Notification);
     method public android.graphics.drawable.Icon? getAnimatedIcon();
+    method public String? getCategory();
     method public androidx.core.content.LocusIdCompat? getLocusId();
     method public int getOngoingActivityId();
     method public android.graphics.drawable.Icon getStaticIcon();
     method public androidx.wear.ongoingactivity.OngoingActivityStatus? getStatus();
+    method public long getTimestamp();
     method public android.app.PendingIntent getTouchIntent();
     method public static boolean hasOngoingActivity(android.app.Notification);
   }
 
-  @androidx.versionedparcelable.VersionedParcelize public abstract class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
-    ctor public OngoingActivityStatus();
-    method public abstract long getNextChangeTimeMillis(long);
-    method public abstract CharSequence getText(android.content.Context, long);
+  @androidx.versionedparcelable.VersionedParcelize public class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
+    method public long getNextChangeTimeMillis(long);
+    method public CharSequence getText(android.content.Context, long);
   }
 
   @androidx.versionedparcelable.VersionedParcelize public class TextOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
     ctor public TextOngoingActivityStatus(String);
-    method public long getNextChangeTimeMillis(long);
-    method public CharSequence getText(android.content.Context, long);
   }
 
   @androidx.versionedparcelable.VersionedParcelize public class TimerOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
@@ -128,9 +129,7 @@
     ctor public TimerOngoingActivityStatus(long, boolean, long);
     ctor public TimerOngoingActivityStatus(long, boolean);
     ctor public TimerOngoingActivityStatus(long);
-    method public long getNextChangeTimeMillis(long);
     method public long getPausedAtMillis();
-    method public CharSequence getText(android.content.Context, long);
     method public long getTimeZeroMillis();
     method public long getTotalDurationMillis();
     method public boolean hasTotalDuration();
@@ -321,14 +320,13 @@
   }
 
   public static interface WearArcLayout.ArcLayoutWidget {
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getSweepAngleDegrees();
     method public int getThicknessPx();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
   }
 
-  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
     ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
     ctor public WearArcLayout.LayoutParams(int, int);
     ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -349,7 +347,7 @@
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getAnchorAngleDegrees();
     method public int getAnchorType();
     method public boolean getClockwise();
@@ -365,7 +363,6 @@
     method public float getTextSize();
     method public int getThicknessPx();
     method public android.graphics.Typeface? getTypeface();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
     method public void setAnchorAngleDegrees(float);
     method public void setAnchorType(@androidx.wear.widget.WearArcLayout.AnchorType int);
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index eae997b..09fdd9a 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -34,6 +34,8 @@
 
     implementation "androidx.core:core-ktx:1.5.0-alpha04"
 
+    annotationProcessor(project(":versionedparcelable:versionedparcelable-compiler"))
+
     compileOnly fileTree(dir: '../wear_stubs', include: ['com.google.android.wearable-stubs.jar'])
 }
 
diff --git a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java
index 2acbbcd..fe6c193 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java
@@ -19,9 +19,9 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
-import androidx.test.rule.ActivityTestRule;
 import androidx.wear.widget.util.WakeLockRule;
 
 import com.google.android.wearable.compat.WearableActivityController;
@@ -37,12 +37,27 @@
     public final WakeLockRule mWakeLock = new WakeLockRule();
 
     @Rule
-    public final ActivityTestRule<AmbientModeSupportResumeTestActivity> mActivityRule =
-            new ActivityTestRule<>(AmbientModeSupportResumeTestActivity.class);
+    public final ActivityScenarioRule<AmbientModeSupportResumeTestActivity> mActivityRule =
+            new ActivityScenarioRule<>(AmbientModeSupportResumeTestActivity.class);
 
     @Test
-    public void testActivityDefaults() throws Throwable {
+    public void testActivityDefaults()  {
         assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
         assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled());
     }
+
+    @Test
+    public void testActivityAutoResume() {
+        assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+        // Test disable/enable auto resume with ambient mode disabled
+        assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled());
+        mActivityRule.getScenario().onActivity(activity-> {
+            activity.getAmbientController().setAutoResumeEnabled(false);
+            assertFalse(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+            activity.getAmbientController().setAutoResumeEnabled(true);
+            assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+        });
+    }
 }
diff --git a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java
index 9571ae7..b7b49c9 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java
@@ -27,4 +27,8 @@
         super.onCreate(savedInstanceState);
         mAmbientController = AmbientModeSupport.attach(this);
     }
+
+    public AmbientModeSupport.AmbientController getAmbientController() {
+        return mAmbientController;
+    }
 }
diff --git a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java
index 271412e..4b15b7a 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java
@@ -19,9 +19,9 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.rule.ActivityTestRule;
 import androidx.wear.widget.util.WakeLockRule;
 
 import com.google.android.wearable.compat.WearableActivityController;
@@ -37,51 +37,51 @@
     public final WakeLockRule mWakeLock = new WakeLockRule();
 
     @Rule
-    public final ActivityTestRule<AmbientModeSupportTestActivity> mActivityRule =
-            new ActivityTestRule<>(AmbientModeSupportTestActivity.class);
+    public final ActivityScenarioRule<AmbientModeSupportTestActivity> mActivityRule =
+            new ActivityScenarioRule<>(AmbientModeSupportTestActivity.class);
 
     @Test
-    public void testEnterAmbientCallback() throws Throwable {
-        AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
-        WearableActivityController.getLastInstance().enterAmbient();
-        assertTrue(activity.mEnterAmbientCalled);
-        assertFalse(activity.mUpdateAmbientCalled);
-        assertFalse(activity.mExitAmbientCalled);
-        assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+    public void testEnterAmbientCallback() {
+        mActivityRule.getScenario().onActivity(activity-> {
+            WearableActivityController.getLastInstance().enterAmbient();
+            assertTrue(activity.mEnterAmbientCalled);
+            assertFalse(activity.mUpdateAmbientCalled);
+            assertFalse(activity.mExitAmbientCalled);
+            assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+        });
     }
 
     @Test
-    public void testUpdateAmbientCallback() throws Throwable {
-        AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
-        WearableActivityController.getLastInstance().updateAmbient();
-        assertFalse(activity.mEnterAmbientCalled);
-        assertTrue(activity.mUpdateAmbientCalled);
-        assertFalse(activity.mExitAmbientCalled);
-        assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+    public void testUpdateAmbientCallback() {
+        mActivityRule.getScenario().onActivity(activity-> {
+            WearableActivityController.getLastInstance().updateAmbient();
+            assertFalse(activity.mEnterAmbientCalled);
+            assertTrue(activity.mUpdateAmbientCalled);
+            assertFalse(activity.mExitAmbientCalled);
+            assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+        });
     }
 
     @Test
-    public void testExitAmbientCallback() throws Throwable {
-        AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
-        WearableActivityController.getLastInstance().exitAmbient();
-        assertFalse(activity.mEnterAmbientCalled);
-        assertFalse(activity.mUpdateAmbientCalled);
-        assertTrue(activity.mExitAmbientCalled);
-        assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+    public void testExitAmbientCallback() {
+        mActivityRule.getScenario().onActivity(activity-> {
+            WearableActivityController.getLastInstance().exitAmbient();
+            assertFalse(activity.mEnterAmbientCalled);
+            assertFalse(activity.mUpdateAmbientCalled);
+            assertTrue(activity.mExitAmbientCalled);
+            assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+        });
     }
 
     @Test
-    public void testAmbientOffloadInvalidatedCallback() throws Throwable {
-        AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
-        WearableActivityController.getLastInstance().invalidateAmbientOffload();
-        assertFalse(activity.mEnterAmbientCalled);
-        assertFalse(activity.mUpdateAmbientCalled);
-        assertFalse(activity.mExitAmbientCalled);
-        assertTrue(activity.mAmbientOffloadInvalidatedCalled);
+    public void testAmbientOffloadInvalidatedCallback() {
+        mActivityRule.getScenario().onActivity(activity-> {
+            WearableActivityController.getLastInstance().invalidateAmbientOffload();
+            assertFalse(activity.mEnterAmbientCalled);
+            assertFalse(activity.mUpdateAmbientCalled);
+            assertFalse(activity.mExitAmbientCalled);
+            assertTrue(activity.mAmbientOffloadInvalidatedCalled);
+        });
     }
 
     @Test
@@ -91,23 +91,38 @@
 
     @Test
     public void testCallsControllerIsAmbient() {
-        AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
+        mActivityRule.getScenario().onActivity(activity-> {
+            WearableActivityController.getLastInstance().setAmbient(true);
+            assertTrue(activity.getAmbientController().isAmbient());
 
-        WearableActivityController.getLastInstance().setAmbient(true);
-        assertTrue(activity.getAmbientController().isAmbient());
-
-        WearableActivityController.getLastInstance().setAmbient(false);
-        assertFalse(activity.getAmbientController().isAmbient());
+            WearableActivityController.getLastInstance().setAmbient(false);
+            assertFalse(activity.getAmbientController().isAmbient());
+        });
     }
 
     @Test
     public void testEnableAmbientOffload() {
-        AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
+        mActivityRule.getScenario().onActivity(activity-> {
+            activity.getAmbientController().setAmbientOffloadEnabled(true);
+            assertTrue(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
 
-        activity.getAmbientController().setAmbientOffloadEnabled(true);
-        assertTrue(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
+            activity.getAmbientController().setAmbientOffloadEnabled(false);
+            assertFalse(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
+        });
+    }
 
-        activity.getAmbientController().setAmbientOffloadEnabled(false);
-        assertFalse(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
+    @Test
+    public void testActivityEnableAutoResume() throws Throwable {
+        assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+        // Test disable/enable auto resume with ambient mode enabled
+        assertTrue(WearableActivityController.getLastInstance().isAmbientEnabled());
+        mActivityRule.getScenario().onActivity(activity-> {
+            activity.getAmbientController().setAutoResumeEnabled(false);
+            assertFalse(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+            activity.getAmbientController().setAutoResumeEnabled(true);
+            assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+        });
     }
 }
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
index d6b8bb7..39f35a4 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
@@ -23,19 +23,21 @@
 import static androidx.wear.widget.util.AsyncViewActions.waitForMatchingView;
 
 import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.view.KeyEvent;
 import android.view.View;
 
 import androidx.annotation.IdRes;
 import androidx.annotation.Nullable;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.espresso.matcher.ViewMatchers;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
 import androidx.wear.test.R;
 import androidx.wear.widget.util.WakeLockRule;
 
@@ -53,131 +55,198 @@
     @Rule
     public final WakeLockRule wakeLock = new WakeLockRule();
 
-    @Rule
-    public final ActivityTestRule<DismissibleFrameLayoutTestActivity> activityRule =
-            new ActivityTestRule<>(
-                    DismissibleFrameLayoutTestActivity.class,
-                    true, /** initial touch mode */
-                    false /** launchActivity */
-            );
-
     @Test
     public void testBackDismiss() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(true, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN back button pressed
-        sendBackKey();
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, true, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN back button pressed
+            sendBackKey();
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+            // Back button up event is consumed, and not pass to the activity
+            scenario.onActivity(activity -> {
+                assertFalse(activity.mConsumeBackButtonUp);
+            });
+        }
     }
 
     @Test
     public void testBackNotDismissIfDisabled() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(false, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN back button pressed
-        sendBackKey();
-        // AND the layout is still nor hidden
-        assertNotHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, false, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN back button pressed
+            sendBackKey();
+            // AND the layout is still not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // Back button up event is not consumed, and continue to pass to the activity
+            scenario.onActivity(activity -> {
+                assertTrue(activity.mConsumeBackButtonUp);
+            });
+        }
     }
 
     @Test
     public void testSwipeDismiss() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(false, true, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN perform a swipe dismiss
-        onView(withId(R.id.dismissible_root)).perform(swipeRight());
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, false, true, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN perform a swipe dismiss
+            onView(withId(R.id.dismissible_root)).perform(swipeRight());
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+        }
     }
 
     @Test
     public void testSwipeNotDismissIfDisabled() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(false, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN perform a swipe dismiss
-        onView(withId(R.id.dismissible_root)).perform(swipeRight());
-        // AND the layout is still nor hidden
-        assertNotHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, false, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN perform a swipe dismiss
+            onView(withId(R.id.dismissible_root)).perform(swipeRight());
+            // AND the layout is still nor hidden
+            assertNotHidden(R.id.dismissible_root);
+        }
+    }
+
+    @Test
+    public void testDisableThenEnableBackDismiss() {
+        // GIVEN a freshly setup DismissibleFrameLayout
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            final DismissibleFrameLayout[] testLayout = new DismissibleFrameLayout[1];
+            final DismissibleFrameLayoutTestActivity[] testActivity =
+                    new DismissibleFrameLayoutTestActivity[1];
+            scenario.onActivity(activity -> {
+                testActivity[0] = activity;
+                testLayout[0] =
+                        (DismissibleFrameLayout) activity.findViewById(R.id.dismissible_root);
+                testLayout[0].registerCallback(mDismissCallback);
+                // Disable back button dismiss
+                testLayout[0].setBackButtonDismissible(false);
+            });
+
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // The layout is not focused
+            assertFalse(testActivity[0].getCurrentFocus() == testLayout[0]);
+            // WHEN back button pressed
+            testActivity[0].mConsumeBackButtonUp = false;
+            sendBackKey();
+            // AND the layout is still not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // Back button up event is not consumed, and continue to pass to the activity
+            assertTrue(testActivity[0].mConsumeBackButtonUp);
+
+            // Enable backButton dismiss, we have to run this on the main thread
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    testLayout[0].setBackButtonDismissible(true);
+                }
+            });
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // The layout is focused
+            assertTrue(testActivity[0].getCurrentFocus() == testLayout[0]);
+            // WHEN back button pressed
+            testActivity[0].mConsumeBackButtonUp = false;
+            sendBackKey();
+            // AND the layout is hidden
+            assertHidden(R.id.dismissible_root);
+            // Back button up event is consumed without passing up to the activity
+            assertFalse(testActivity[0].mConsumeBackButtonUp);
+        }
     }
 
 
     @Test
     public void testBackDismissWithRecyclerView() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayoutWithRecyclerView(
-                true, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN back button pressed
-        sendBackKey();
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutWithRecyclerViewIntent())) {
+            configureDismissibleLayout(scenario, true, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN back button pressed
+            sendBackKey();
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+            // Back button up event is consumed, and not pass to the activity
+            scenario.onActivity(activity -> {
+                assertFalse(activity.mConsumeBackButtonUp);
+            });
+        }
     }
 
     @Test
     public void testSwipeDismissWithRecyclerView() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayoutWithRecyclerView(
-                false, true, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN perform a swipe dismiss
-        onView(withId(R.id.dismissible_root)).perform(swipeRight());
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutWithRecyclerViewIntent())) {
+            configureDismissibleLayout(scenario, false, true, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN perform a swipe dismiss
+            onView(withId(R.id.dismissible_root)).perform(swipeRight());
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+        }
     }
 
     /**
-     * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
-     * a single static child.
+     * Creates intent for launching an activity for test cases - a {@link SwipeDismissFrameLayout}
+     * with a single static child.
      */
-    private void setUpDismissibleLayout(
-            boolean backDismissible,
-            boolean swipeable,
-            @Nullable DismissibleFrameLayout.Callback callback) {
-        activityRule.launchActivity(
-                new Intent()
-                        .putExtra(
-                                LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                                androidx.wear.test.R.layout.dismissible_frame_layout_testcase));
-
-        configureDismissibleLayout(backDismissible, swipeable, callback);
+    private Intent createDismissibleLayoutIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.dismissible_frame_layout_testcase);
     }
 
-    private void setUpDismissibleLayoutWithRecyclerView(
-            boolean backDismissible,
-            boolean swipeable,
-            @Nullable DismissibleFrameLayout.Callback callback) {
-        Intent launchIntent = new Intent();
-        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                R.layout.dismissible_frame_layout_recyclerview_testcase);
-        launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
-        activityRule.launchActivity(launchIntent);
-
-        configureDismissibleLayout(backDismissible, swipeable, callback);
+    /**
+     * Creates intent for launching an activity for test cases - a {@link SwipeDismissFrameLayout}
+     * with a child of scrollable container.
+     */
+    private Intent createDismissibleLayoutWithRecyclerViewIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.dismissible_frame_layout_recyclerview_testcase)
+                .putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
     }
 
     private void configureDismissibleLayout(
+            ActivityScenario<DismissibleFrameLayoutTestActivity> scenario,
             boolean backDismissible,
             boolean swipeable,
             @Nullable DismissibleFrameLayout.Callback callback) {
-        Activity activity = activityRule.getActivity();
-        DismissibleFrameLayout testLayout = activity.findViewById(R.id.dismissible_root);
-        testLayout.setBackButtonDismissible(backDismissible);
-        testLayout.setSwipeDismissible(swipeable);
-
-        if (callback != null) {
-            testLayout.registerCallback(callback);
-        }
+        scenario.onActivity(activity -> {
+            DismissibleFrameLayout testLayout = activity.findViewById(R.id.dismissible_root);
+            testLayout.setBackButtonDismissible(backDismissible);
+            testLayout.setSwipeDismissible(swipeable);
+            if (callback != null) {
+                testLayout.registerCallback(callback);
+            }
+        });
     }
 
     private static void assertHidden(@IdRes int layoutId) {
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
index f9132b3..2b2158c 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
@@ -29,6 +30,7 @@
 public class DismissibleFrameLayoutTestActivity extends LayoutTestActivity {
 
     public static final String EXTRA_LAYOUT_HORIZONTAL = "layout_horizontal";
+    public boolean mConsumeBackButtonUp = false;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -54,6 +56,14 @@
         recyclerView.setAdapter(new MyRecyclerViewAdapter());
     }
 
+    public boolean onKeyUp(int keyCode, KeyEvent evnet) {
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            mConsumeBackButtonUp = true;
+            return true;
+        }
+        return false;
+    }
+
     private static class MyRecyclerViewAdapter
             extends RecyclerView.Adapter<MyRecyclerViewAdapter.CustomViewHolder> {
         @Override
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
index e493360..472b86e 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
@@ -27,13 +27,14 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.graphics.RectF;
 import android.view.View;
 
 import androidx.annotation.IdRes;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.espresso.ViewAction;
 import androidx.test.espresso.action.GeneralLocation;
 import androidx.test.espresso.action.GeneralSwipeAction;
@@ -44,7 +45,6 @@
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
 import androidx.wear.test.R;
 import androidx.wear.widget.util.ArcSwipe;
 import androidx.wear.widget.util.FrameLocationAvoidingEdges;
@@ -65,14 +65,6 @@
     @Rule
     public final WakeLockRule wakeLock = new WakeLockRule();
 
-    @Rule
-    public final ActivityTestRule<DismissibleFrameLayoutTestActivity> activityRule =
-            new ActivityTestRule<>(
-                    DismissibleFrameLayoutTestActivity.class,
-                    true, /** initial touch mode */
-                    false /** launchActivity */
-            );
-
     private int mLayoutWidth;
     private int mLayoutHeight;
     private int mXPositionOnScreen;
@@ -81,228 +73,265 @@
     @Test
     public void testCanScrollHorizontally() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        testLayout.setSwipeable(true);
-        // WHEN we check that the layout is horizontally scrollable from left to right.
-        // THEN the layout is found to be horizontally swipeable from left to right.
-        assertTrue(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                testLayout.setSwipeable(true);
+                // WHEN we check that the layout is horizontally scrollable from left to right.
+                // THEN the layout is found to be horizontally swipeable from left to right.
+                assertTrue(testLayout.canScrollHorizontally(-20));
+                // AND the layout is found to NOT be horizontally swipeable from right to left.
+                assertFalse(testLayout.canScrollHorizontally(20));
 
-        // WHEN we switch off the swipe-to-dismiss functionality for the layout
-        testLayout.setSwipeable(false);
-        // THEN the layout is found NOT to be horizontally swipeable from left to right.
-        assertFalse(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+                // WHEN we switch off the swipe-to-dismiss functionality for the layout
+                testLayout.setSwipeable(false);
+                // THEN the layout is found NOT to be horizontally swipeable from left to right.
+                assertFalse(testLayout.canScrollHorizontally(-20));
+                // AND the layout is found to NOT be horizontally swipeable from right to left.
+                assertFalse(testLayout.canScrollHorizontally(20));
+            });
+        }
     }
 
     @Test
     public void canScrollHorizontallyShouldBeFalseWhenInvisible() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
-        // GIVEN the layout is invisible
-        // Note: We have to run this on the main thread, because of thread checks in View.java.
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                testLayout.setVisibility(View.INVISIBLE);
-            }
-        });
-        // WHEN we check that the layout is horizontally scrollable
-        // THEN the layout is found to be NOT horizontally swipeable from left to right.
-        assertFalse(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            final SwipeDismissFrameLayout[] testLayout = new SwipeDismissFrameLayout[1];
+            scenario.onActivity(activity -> {
+                testLayout[0] =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+            });
+            // GIVEN the layout is invisible
+            // Note: We have to run this on the main thread, because of thread checks in View.java.
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    testLayout[0].setVisibility(View.INVISIBLE);
+                }
+            });
+            // WHEN we check that the layout is horizontally scrollable
+            // THEN the layout is found to be NOT horizontally swipeable from left to right.
+            assertFalse(testLayout[0].canScrollHorizontally(-20));
+            // AND the layout is found to NOT be horizontally swipeable from right to left.
+            assertFalse(testLayout[0].canScrollHorizontally(20));
+        }
     }
 
     @Test
     public void canScrollHorizontallyShouldBeFalseWhenGone() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        final SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        // GIVEN the layout is gone
-        // Note: We have to run this on the main thread, because of thread checks in View.java.
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                testLayout.setVisibility(View.GONE);
-            }
-        });
-        // WHEN we check that the layout is horizontally scrollable
-        // THEN the layout is found to be NOT horizontally swipeable from left to right.
-        assertFalse(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            final SwipeDismissFrameLayout[] testLayout = new SwipeDismissFrameLayout[1];
+            scenario.onActivity(activity -> {
+                testLayout[0] =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+            });
+            // GIVEN the layout is gone
+            // Note: We have to run this on the main thread, because of thread checks in View.java.
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    testLayout[0].setVisibility(View.GONE);
+                }
+            });
+            // WHEN we check that the layout is horizontally scrollable
+            // THEN the layout is found to be NOT horizontally swipeable from left to right.
+            assertFalse(testLayout[0].canScrollHorizontally(-20));
+            // AND the layout is found to NOT be horizontally swipeable from right to left.
+            assertFalse(testLayout[0].canScrollHorizontally(20));
+        }
     }
 
     @Test
     public void testSwipeDismissDisabledByDefault() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        // WHEN we check that the layout is dismissible
-        // THEN the layout is find to be dismissible
-        assertFalse(testLayout.isSwipeable());
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                // WHEN we check that the layout is dismissible
+                // THEN the layout is find to be dismissible
+                assertFalse(testLayout.isSwipeable());
+            });
+        }
     }
 
     @Test
     public void testSwipeDismissesViewIfEnabled() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        ((SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root))
-                .setSwipeable(true);
-        // WHEN we perform a swipe to dismiss
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
-        // AND hidden
-        assertHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            setUpSwipeableAndCallback(scenario, true);
+            // WHEN we perform a swipe to dismiss
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+            // AND hidden
+            assertHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     public void testSwipeDoesNotDismissViewIfDisabled() {
         // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        testLayout.setSwipeable(false);
-        // WHEN we perform a swipe to dismiss
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
-        // THEN the layout is not hidden
-        assertNotHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            setUpSwipeableAndCallback(scenario, false);
+            // WHEN we perform a swipe to dismiss
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+            // THEN the layout is not hidden
+            assertNotHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     public void testAddRemoveCallback() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
-        // WHEN we remove the swipe callback
-        testLayout.removeCallback(mDismissCallback);
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
-        // THEN the layout is not hidden
-        assertNotHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            setUpSwipeableAndCallback(scenario, true);
+            // WHEN we remove the swipe callback
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                testLayout.removeCallback(mDismissCallback);
+            });
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+            // THEN the layout is not hidden
+            assertNotHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable {
         // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
-        setUpSwipeDismissWithHorizontalRecyclerView();
-        activityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                Activity activity = activityRule.getActivity();
-                RecyclerView testLayout = activity.findViewById(R.id.recycler_container);
-                // Scroll to a position from which the child is scrollable.
-                testLayout.scrollToPosition(50);
-            }
-        });
-
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-        // WHEN we perform a swipe to dismiss from the center of the screen.
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
-        // THEN the layout is not hidden
-        assertNotHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(
+                             createSwipeDismissWithHorizontalRecyclerViewLaunchIntent()
+                     )) {
+            setUpSwipeableAndCallback(scenario, true);
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                RecyclerView testRecyclerView = activity.findViewById(R.id.recycler_container);
+                testRecyclerView.scrollToPosition(50);
+            });
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+            // WHEN we perform a swipe to dismiss from the center of the screen.
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
+            // THEN the layout is not hidden
+            assertNotHidden(R.id.swipe_dismiss_root);
+        }
     }
 
 
     @Test
     public void testEdgeSwipeDoesDismissViewIfScrollable() {
         // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
-        setUpSwipeDismissWithHorizontalRecyclerView();
-        Activity activity = activityRule.getActivity();
-        ((SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root))
-                .setSwipeable(true);
-        // WHEN we perform a swipe to dismiss from the left edge of the screen.
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftCenterAvoidingEdge());
-        // THEN the layout is hidden
-        assertHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(
+                             createSwipeDismissWithHorizontalRecyclerViewLaunchIntent()
+                     )) {
+            setUpSwipeableAndCallback(scenario, true);
+            // WHEN we perform a swipe to dismiss from the left edge of the screen.
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftCenterAvoidingEdge());
+            // THEN the layout is hidden
+            assertHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     @FlakyTest
     public void testArcSwipeDoesNotTriggerDismiss() {
         // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content
-        setUpSwipeDismissWithVerticalRecyclerView();
-        int center = mLayoutHeight / 2;
-        int halfBound = mLayoutWidth / 2;
-        RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
-        // WHEN the view is scrolled on an arc from top to bottom.
-        onView(withId(R.id.swipe_dismiss_root)).perform(
-                swipeTopFromBottomOnArcAvoidingEdge(bounds));
-        // THEN the layout is not dismissed and not hidden.
-        assertNotHidden(R.id.swipe_dismiss_root);
-        // AND the content view is scrolled.
-        assertScrolledY(R.id.recycler_container);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(
+                             createSwipeDismissWithVerticalRecyclerViewLaunchIntent()
+                     )) {
+            setUpSwipeableAndCallback(scenario, true);
+            int center = mLayoutHeight / 2;
+            int halfBound = mLayoutWidth / 2;
+            RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
+            // WHEN the view is scrolled on an arc from top to bottom.
+            onView(withId(R.id.swipe_dismiss_root)).perform(
+                    swipeTopFromBottomOnArcAvoidingEdge(bounds));
+            // THEN the layout is not dismissed and not hidden.
+            assertNotHidden(R.id.swipe_dismiss_root);
+            // AND the content view is scrolled.
+            assertScrolledY(R.id.recycler_container);
+        }
     }
 
     /**
-     * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
-     * a single static child.
+     * Creates intent for launching the simplest possible layout for test cases - a
+     * {@link SwipeDismissFrameLayout} with a single static child.
      */
-    private void setUpSimpleLayout() {
-        activityRule.launchActivity(
-                new Intent()
-                        .putExtra(
-                                LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                                R.layout.swipe_dismiss_layout_testcase_1));
-        setDismissCallback();
+    private Intent createSimpleLayoutLaunchIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.swipe_dismiss_layout_testcase_1);
     }
 
 
     /**
-     * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
-     * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link
-     * RecyclerView} as a child, ready to accept an adapter.
+     * Creates intent for launching a slightly more involved layout for testing swipe-to-dismiss
+     * with scrollable containers. This layout contains a {@link SwipeDismissFrameLayout} with a
+     * horizontal {@link RecyclerView} as a child, ready to accept an adapter.
      */
-    private void setUpSwipeDismissWithHorizontalRecyclerView() {
-        Intent launchIntent = new Intent();
-        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                R.layout.swipe_dismiss_layout_testcase_2);
-        launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
-        activityRule.launchActivity(launchIntent);
-        setDismissCallback();
+    private Intent createSwipeDismissWithHorizontalRecyclerViewLaunchIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.swipe_dismiss_layout_testcase_2)
+                .putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
     }
 
     /**
-     * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
-     * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link
-     * WearableRecyclerView} as a child, ready to accept an adapter.
+     * Creates intent for launching slightly more involved layout for testing swipe-to-dismiss
+     * with scrollable containers. This layout contains a {@link SwipeDismissFrameLayout} with a
+     * vertical {@link WearableRecyclerView} as a child, ready to accept an adapter.
      */
-    private void setUpSwipeDismissWithVerticalRecyclerView() {
-        Intent launchIntent = new Intent();
-        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                R.layout.swipe_dismiss_layout_testcase_2);
+    private Intent createSwipeDismissWithVerticalRecyclerViewLaunchIntent() {
+        Intent launchIntent = new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.swipe_dismiss_layout_testcase_2);
         launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false);
-        activityRule.launchActivity(launchIntent);
-        setDismissCallback();
+
+        return launchIntent;
     }
 
-    private void setDismissCallback() {
-        setCallback(mDismissCallback);
-    }
-
-    private void setCallback(SwipeDismissFrameLayout.Callback callback) {
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
+    private void setDismissCallback(SwipeDismissFrameLayout testLayout) {
         int[] locationOnScreen = new int[2];
         testLayout.getLocationOnScreen(locationOnScreen);
         mXPositionOnScreen = locationOnScreen[0];
         mYPositionOnScreen = locationOnScreen[1];
         mLayoutWidth = testLayout.getWidth();
         mLayoutHeight = testLayout.getHeight();
-        testLayout.addCallback(callback);
+        testLayout.addCallback(mDismissCallback);
+    }
+
+    private void setUpSwipeableAndCallback(
+            ActivityScenario<DismissibleFrameLayoutTestActivity> scenario,
+            boolean swipeable
+    ) {
+        scenario.onActivity(activity -> {
+            SwipeDismissFrameLayout testLayout =
+                    (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+            setDismissCallback(testLayout);
+            testLayout.setSwipeable(swipeable);
+        });
     }
 
     private static void assertHidden(@IdRes int layoutId) {
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
index 465ec78..57f0901 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
@@ -31,6 +31,7 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.TextView
+import androidx.core.view.children
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.espresso.Espresso
@@ -49,6 +50,9 @@
 import androidx.test.screenshot.assertAgainstGolden
 import androidx.wear.test.R
 import androidx.wear.widget.util.AsyncViewActions.waitForMatchingView
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_CENTER
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_OUTER
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_INNER
 import org.hamcrest.CoreMatchers.allOf
 import org.hamcrest.CoreMatchers.any
 import org.hamcrest.Matcher
@@ -223,6 +227,100 @@
         )
     }
 
+    // Extension functions to make the margin test more readable.
+    fun WearArcLayout.addSeparator() {
+        addView(
+            WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
+                text = " "
+                minSweepDegrees = 10f
+                setBackgroundColor(Color.rgb(100, 100, 100))
+                clockwise = true
+                textSize = 40f
+            }
+        )
+    }
+
+    fun WearArcLayout.addText(
+        text0: String,
+        color: Int,
+        marginLeft: Int = 0,
+        marginTop: Int = 0,
+        marginRight: Int = 0,
+        marginBottom: Int = 0,
+        vAlign: Int = VALIGN_CENTER
+    ) {
+        addView(
+            WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
+                text = text0
+                setBackgroundColor(color)
+                clockwise = true
+                textSize = 14f
+                layoutParams = WearArcLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT
+                ).apply {
+                    setMargins(marginLeft, marginTop, marginRight, marginBottom)
+                    verticalAlignment = vAlign
+                }
+            }
+        )
+    }
+
+    private fun createArcWithMargin() =
+        WearArcLayout(ApplicationProvider.getApplicationContext()).apply {
+            anchorType = WearArcLayout.ANCHOR_CENTER
+            addSeparator()
+            addText("RI", Color.RED, marginTop = 16, vAlign = VALIGN_INNER)
+            addText("GI", Color.GREEN, marginTop = 8, marginBottom = 8, vAlign = VALIGN_INNER)
+            addText("BI", Color.BLUE, marginBottom = 16, vAlign = VALIGN_INNER)
+            addSeparator()
+            addText("Red", Color.RED, marginTop = 16)
+            addText("Green", Color.GREEN, marginTop = 8, marginBottom = 8)
+            addText("Blue", Color.BLUE, marginBottom = 16)
+            addSeparator()
+            addText("RO", Color.RED, marginTop = 16, vAlign = VALIGN_OUTER)
+            addText("GO", Color.GREEN, marginTop = 8, marginBottom = 8, vAlign = VALIGN_OUTER)
+            addText("BO", Color.BLUE, marginBottom = 16, vAlign = VALIGN_OUTER)
+            addSeparator()
+            addText("L", Color.WHITE, marginRight = 20)
+            addSeparator()
+            addText("C", Color.WHITE, marginRight = 10, marginLeft = 10)
+            addSeparator()
+            addText("R", Color.WHITE, marginLeft = 20)
+            addSeparator()
+        }
+
+    private fun createTwoArcsWithMargin() = listOf(
+        // First arc goes on top
+        createArcWithMargin(),
+
+        // Second arc in the bottom, and we change al children to go counterclockwise.
+        createArcWithMargin().apply {
+            anchorAngleDegrees = 180f
+            children.forEach {
+                (it as? WearCurvedTextView) ?.clockwise = false
+            }
+        }
+    )
+
+    @Test
+    fun testMargins() {
+        doOneTest(
+            "margin_test",
+            createTwoArcsWithMargin()
+        )
+    }
+
+    @Test
+    fun testMarginsCcw() {
+        doOneTest(
+            "margin_ccw_test",
+            createTwoArcsWithMargin().map {
+                it.apply { clockwise = false }
+            }
+        )
+    }
+
     // Generates a click in the x,y coordinates in the view's coordinate system.
     fun customClick(x: Float, y: Float) = ViewActions.actionWithAssertions(
         GeneralClickAction(
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
index 20ff4f1..c09c06a 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
@@ -227,12 +227,14 @@
                     text = "This is a clockwise text for testing background"
                     clockwise = true
                     anchorAngleDegrees = 170.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 },
                 WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
                     text = "Another clockwise text"
                     clockwise = true
                     anchorAngleDegrees = 70.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 }
             )
@@ -249,12 +251,14 @@
                     text = "This is a counterclockwise text for testing background"
                     clockwise = false
                     anchorAngleDegrees = 100.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 },
                 WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
                     text = "Another counterclockwise text"
                     clockwise = false
                     anchorAngleDegrees = 230.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 }
             )
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
index b5ccb1e..840bb02 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
@@ -28,7 +28,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import android.app.Activity;
+import android.content.res.Configuration;
 import android.view.View;
 
 import androidx.annotation.IdRes;
@@ -38,10 +38,10 @@
 import androidx.test.espresso.action.GeneralSwipeAction;
 import androidx.test.espresso.action.Press;
 import androidx.test.espresso.action.Swipe;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
 import androidx.wear.test.R;
 import androidx.wear.widget.util.WakeLockRule;
 
@@ -64,8 +64,8 @@
     public final WakeLockRule wakeLock = new WakeLockRule();
 
     @Rule
-    public final ActivityTestRule<WearableRecyclerViewTestActivity> mActivityRule =
-            new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true);
+    public final ActivityScenarioRule<WearableRecyclerViewTestActivity> mActivityRule =
+            new ActivityScenarioRule<>(WearableRecyclerViewTestActivity.class);
 
     @Before
     public void setUp() {
@@ -74,88 +74,117 @@
 
     @Test
     public void testCaseInitState() {
-        WearableRecyclerView wrv = new WearableRecyclerView(mActivityRule.getActivity());
-        wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
+        mActivityRule.getScenario().onActivity(activity -> {
+            WearableRecyclerView wrv = new WearableRecyclerView(activity);
+            wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
 
-        assertFalse(wrv.isEdgeItemsCenteringEnabled());
-        assertFalse(wrv.isCircularScrollingGestureEnabled());
-        assertEquals(1.0f, wrv.getBezelFraction(), 0.01f);
-        assertEquals(180.0f, wrv.getScrollDegreesPerScreen(), 0.01f);
+            assertFalse(wrv.isEdgeItemsCenteringEnabled());
+            assertFalse(wrv.isCircularScrollingGestureEnabled());
+            assertEquals(1.0f, wrv.getBezelFraction(), 0.01f);
+            assertEquals(180.0f, wrv.getScrollDegreesPerScreen(), 0.01f);
+        });
     }
 
     @Test
     public void testEdgeItemsCenteringOnAndOff() throws Throwable {
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv =
-                        (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
-                wrv.setEdgeItemsCenteringEnabled(true);
-            }
+
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv =
+                            (WearableRecyclerView) activity.findViewById(R.id.wrv);
+                    wrv.setEdgeItemsCenteringEnabled(true);
+                }
+            });
+        });
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv =
+                            (WearableRecyclerView) activity.findViewById(
+                                    R.id.wrv);
+                    View child = wrv.getChildAt(0);
+                    assertNotNull("child", child);
+                    Configuration configuration = activity.getResources().getConfiguration();
+                    if (configuration.isScreenRound()) {
+                        assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
+                    } else {
+                        assertEquals(0, child.getTop());
+                    }
+                }
+            });
+        });
+
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv =
+                            (WearableRecyclerView) activity.findViewById(
+                                    R.id.wrv);
+                    wrv.setEdgeItemsCenteringEnabled(false);
+                }
+            });
         });
 
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv =
-                        (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
-                View child = wrv.getChildAt(0);
-                assertNotNull("child", child);
-                assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
-            }
-        });
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv =
+                            (WearableRecyclerView) activity.findViewById(
+                                    R.id.wrv);
+                    View child = wrv.getChildAt(0);
+                    assertNotNull("child", child);
+                    assertEquals(0, child.getTop());
 
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv =
-                        (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
-                wrv.setEdgeItemsCenteringEnabled(false);
-            }
-        });
-
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv =
-                        (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
-                View child = wrv.getChildAt(0);
-                assertNotNull("child", child);
-                assertEquals(0, child.getTop());
-
-            }
+                }
+            });
         });
     }
 
     @Test
     public void testEdgeItemsCenteringBeforeChildrenDrawn() throws Throwable {
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                Activity activity = mActivityRule.getActivity();
-                WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv);
-                RecyclerView.Adapter<WearableRecyclerView.ViewHolder> adapter = wrv.getAdapter();
-                wrv.setAdapter(null);
-                wrv.setEdgeItemsCenteringEnabled(true);
-                wrv.setAdapter(adapter);
-            }
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(
+                            R.id.wrv);
+                    RecyclerView.Adapter<WearableRecyclerView.ViewHolder> adapter =
+                            wrv.getAdapter();
+                    wrv.setAdapter(null);
+                    wrv.setEdgeItemsCenteringEnabled(true);
+                    wrv.setAdapter(adapter);
+                }
+            });
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv =
-                        (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
-                // Verify the first child
-                View child = wrv.getChildAt(0);
-                assertNotNull("child", child);
-                assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
-            }
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv =
+                            (WearableRecyclerView) activity.findViewById(
+                                    R.id.wrv);
+                    // Verify the first child
+                    View child = wrv.getChildAt(0);
+                    assertNotNull("child", child);
+                    Configuration configuration = activity.getResources().getConfiguration();
+                    if (configuration.isScreenRound()) {
+                        assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
+                    } else {
+                        assertEquals(0, child.getTop());
+                    }
+                }
+            });
         });
     }
 
@@ -163,20 +192,21 @@
     public void testCircularScrollingGesture() throws Throwable {
         onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
         assertNotScrolledY(R.id.wrv);
-        final WearableRecyclerView wrv =
-                (WearableRecyclerView) mActivityRule.getActivity().findViewById(
-                        R.id.wrv);
-        assertFalse(wrv.isCircularScrollingGestureEnabled());
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv = (WearableRecyclerView)
-                        mActivityRule.getActivity().findViewById(R.id.wrv);
-                wrv.setCircularScrollingGestureEnabled(true);
-            }
+        mActivityRule.getScenario().onActivity(activity -> {
+            final WearableRecyclerView wrv =
+                    (WearableRecyclerView) activity.findViewById(
+                            R.id.wrv);
+            assertFalse(wrv.isCircularScrollingGestureEnabled());
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv = (WearableRecyclerView)
+                            activity.findViewById(R.id.wrv);
+                    wrv.setCircularScrollingGestureEnabled(true);
+                }
+            });
+            assertTrue(wrv.isCircularScrollingGestureEnabled());
         });
-        assertTrue(wrv.isCircularScrollingGestureEnabled());
-
         // Explicitly set the swipe to SLOW here to avoid problems with test failures on phone AVDs
         // with "Gesture navigation" enabled. This is not a particularly satisfactory fix to this
         // problem and ideally we should look to move these tests to use a watch AVD which should
@@ -187,36 +217,39 @@
 
     @Test
     public void testCurvedOffsettingHelper() throws Throwable {
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv =
-                        (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
-                wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
-            }
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv =
+                            (WearableRecyclerView) activity.findViewById(
+                                    R.id.wrv);
+                    wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
+                }
+            });
         });
-
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
 
-        mActivityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                Activity activity = mActivityRule.getActivity();
-                WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv);
-                if (activity.getResources().getConfiguration().isScreenRound()) {
-                    View child = wrv.getChildAt(0);
-                    assertTrue(child.getLeft() > 0);
-                } else {
-                    for (int i = 0; i < wrv.getChildCount(); i++) {
-                        assertEquals(0, wrv.getChildAt(i).getLeft());
+        mActivityRule.getScenario().onActivity(activity -> {
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(
+                            R.id.wrv);
+                    if (activity.getResources().getConfiguration().isScreenRound()) {
+                        View child = wrv.getChildAt(0);
+                        assertTrue(child.getLeft() > 0);
+                    } else {
+                        for (int i = 0; i < wrv.getChildCount(); i++) {
+                            assertEquals(0, wrv.getChildAt(i).getLeft());
+                        }
                     }
                 }
-            }
+            });
         });
     }
-
     private static ViewAction swipeDownFromTopRightSlowly() {
         return new GeneralSwipeAction(
                 Swipe.SLOW, GeneralLocation.TOP_RIGHT,
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java b/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java
index fd6146c..b78af94 100644
--- a/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java
@@ -51,7 +51,7 @@
          * method. If they do not, an exception will be thrown.</em>
          *
          * @param ambientDetails bundle containing information about the display being used.
-         *                      It includes information about low-bit color and burn-in protection.
+         *                       It includes information about low-bit color and burn-in protection.
          */
         void onEnterAmbient(Bundle ambientDetails);
 
@@ -81,15 +81,15 @@
     }
 
     AmbientDelegate(@Nullable Activity activity,
-                           @NonNull WearableControllerProvider wearableControllerProvider,
-                           @NonNull AmbientCallback callback) {
+            @NonNull WearableControllerProvider wearableControllerProvider,
+            @NonNull AmbientCallback callback) {
         mActivity = new WeakReference<>(activity);
         mCallback = callback;
         mWearableControllerProvider = wearableControllerProvider;
     }
 
     /**
-     * Receives and handles the onCreate call from the associated {@link AmbientMode}
+     * Receives and handles the onCreate call from the associated {@link AmbientModeSupport}
      */
     void onCreate() {
         Activity activity = mActivity.get();
@@ -103,7 +103,7 @@
     }
 
     /**
-     * Receives and handles the onResume call from the associated {@link AmbientMode}
+     * Receives and handles the onResume call from the associated {@link AmbientModeSupport}
      */
     void onResume() {
         if (mWearableController != null) {
@@ -112,7 +112,7 @@
     }
 
     /**
-     * Receives and handles the onPause call from the associated {@link AmbientMode}
+     * Receives and handles the onPause call from the associated {@link AmbientModeSupport}
      */
     void onPause() {
         if (mWearableController != null) {
@@ -121,7 +121,7 @@
     }
 
     /**
-     * Receives and handles the onStop call from the associated {@link AmbientMode}
+     * Receives and handles the onStop call from the associated {@link AmbientModeSupport}
      */
     void onStop() {
         if (mWearableController != null) {
@@ -130,7 +130,7 @@
     }
 
     /**
-     * Receives and handles the onDestroy call from the associated {@link AmbientMode}
+     * Receives and handles the onDestroy call from the associated {@link AmbientModeSupport}
      */
     void onDestroy() {
         if (mWearableController != null) {
@@ -156,6 +156,18 @@
     }
 
     /**
+     * Sets whether this activity's task should be moved to the front when the system exits
+     * ambient mode. If true, the activity's task may be moved to the front if it was the last
+     * activity to be running when ambient started, depending on how much time the system spent
+     * in ambient mode.
+     */
+    public void setAutoResumeEnabled(boolean enabled) {
+        if (mWearableController != null) {
+            mWearableController.setAutoResumeEnabled(enabled);
+        }
+    }
+
+    /**
      * @return {@code true} if the activity is currently in ambient.
      */
     boolean isAmbient() {
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
index b0eebcf..bf21a45 100644
--- a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
@@ -47,12 +47,12 @@
  * {@link FragmentActivity} and use the {@link AmbientController} can be found below:
  * <p>
  * <pre class="prettyprint">{@code
- *     AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this);
- *     boolean isAmbient =  controller.isAmbient();
+ *     AmbientModeSupport.AmbientController controller = AmbientModeSupport.attach(this);
+ *     boolean isAmbient = controller.isAmbient();
  * }</pre>
  */
 public final class AmbientModeSupport extends Fragment {
-    private static final String TAG = "AmbientMode";
+    private static final String TAG = "AmbientModeSupport";
 
     /**
      * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
@@ -82,11 +82,11 @@
 
     /**
      * Interface for any {@link Activity} that wishes to implement Ambient Mode. Use the
-     * {@link #getAmbientCallback()} method to return and {@link AmbientCallback} which can be used
+     * {@link #getAmbientCallback()} method to return an {@link AmbientCallback} which can be used
      * to bind the {@link AmbientModeSupport} to the instantiation of this interface.
      * <p>
      * <pre class="prettyprint">{@code
-     * return new AmbientMode.AmbientCallback() {
+     * return new AmbientModeSupport.AmbientCallback() {
      *     public void onEnterAmbient(Bundle ambientDetails) {...}
      *     public void onExitAmbient(Bundle ambientDetails) {...}
      *  }
@@ -101,7 +101,8 @@
     }
 
     /**
-     * Callback to receive ambient mode state changes. It must be used by all users of AmbientMode.
+     * Callback to receive ambient mode state changes. It must be used by all users of
+     * AmbientModeSupport.
      */
     public abstract static class AmbientCallback {
         /**
@@ -302,5 +303,17 @@
                 mDelegate.setAmbientOffloadEnabled(enabled);
             }
         }
+
+        /**
+         * Sets whether this activity's task should be moved to the front when the system exits
+         * ambient mode. If true, the activity's task may be moved to the front if it was the
+         * last activity to be running when ambient started, depending on how much time the
+         * system spent in ambient mode.
+         */
+        public void setAutoResumeEnabled(boolean enabled) {
+            if (mDelegate != null) {
+                mDelegate.setAutoResumeEnabled(enabled);
+            }
+        }
     }
 }
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java
index 6ac18bd..f2af9a3 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.graphics.drawable.Icon;
 import android.os.Build;
+import android.os.SystemClock;
 import android.service.notification.StatusBarNotification;
 
 import androidx.annotation.DrawableRes;
@@ -83,6 +84,7 @@
         private PendingIntent mTouchIntent;
         private LocusIdCompat mLocusId;
         private int mOngoingActivityId = OngoingActivityData.DEFAULT_ID;
+        private String mCategory;
 
         /**
          * Construct a new empty {@link Builder}, associated with the given notification.
@@ -106,6 +108,8 @@
          * {@link OngoingActivity}. For example, in the WatchFace.
          * Should be white with a transparent background, preferably an AnimatedVectorDrawable.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setAnimatedIcon(@NonNull Icon animatedIcon) {
             mAnimatedIcon = animatedIcon;
@@ -117,6 +121,8 @@
          * {@link OngoingActivity}. For example, in the WatchFace.
          * Should be white with a transparent background, preferably an AnimatedVectorDrawable.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setAnimatedIcon(@DrawableRes int animatedIcon) {
             mAnimatedIcon = Icon.createWithResource(mContext, animatedIcon);
@@ -128,6 +134,8 @@
          * {@link OngoingActivity}, for example in the WatchFace in ambient mode.
          * Should be white with a transparent background, preferably an VectorDrawable.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setStaticIcon(@NonNull Icon staticIcon) {
             mStaticIcon = staticIcon;
@@ -139,6 +147,8 @@
          * {@link OngoingActivity}, for example in the WatchFace in ambient mode.
          * Should be white with a transparent background, preferably an VectorDrawable.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setStaticIcon(@DrawableRes int staticIcon) {
             mStaticIcon = Icon.createWithResource(mContext, staticIcon);
@@ -149,6 +159,8 @@
          * Set the initial status of this ongoing activity, the status may be displayed on the UI to
          * show progress of the Ongoing Activity.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setStatus(@NonNull OngoingActivityStatus status) {
             mStatus = status;
@@ -159,6 +171,8 @@
          * Set the intent to be used to go back to the activity when the user interacts with the
          * Ongoing Activity in other surfaces (for example, taps the Icon on the WatchFace)
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setTouchIntent(@NonNull PendingIntent touchIntent) {
             mTouchIntent = touchIntent;
@@ -169,6 +183,8 @@
          * Set the corresponding LocusId of this {@link OngoingActivity}, this will be used by the
          * launcher to identify the corresponding launcher item and display it accordingly.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setLocusId(@NonNull LocusIdCompat locusId) {
             mLocusId = locusId;
@@ -179,6 +195,8 @@
          * Give an id to this {@link OngoingActivity}, as a way to reference it in
          * {@link OngoingActivity#fromExistingOngoingActivity(Context, int)}
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
         @NonNull
         public Builder setOngoingActivityId(int ongoingActivityId) {
             mOngoingActivityId = ongoingActivityId;
@@ -186,6 +204,18 @@
         }
 
         /**
+         * Set the category of this {@link OngoingActivity}, this may be used by the system to
+         * prioritize it.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
+        @NonNull
+        public Builder setCategory(@NonNull String category) {
+            mCategory = category;
+            return this;
+        }
+
+        /**
          * Combine all options provided and the information in the notification if needed,
          * return a new {@link OngoingActivity} object.
          *
@@ -220,14 +250,18 @@
                 locusId = Api29Impl.getLocusId(notification);
             }
 
+            String category = mCategory == null ? notification.category : mCategory;
+
             return new OngoingActivity(mNotificationId, mNotificationBuilder,
                     new OngoingActivityData(
                         mAnimatedIcon,
                         staticIcon,
                         status,
                         touchIntent,
-                        locusId,
-                        mOngoingActivityId
+                        locusId == null ? null : locusId.getId(),
+                        mOngoingActivityId,
+                        category,
+                        SystemClock.elapsedRealtime()
                     ));
         }
     }
@@ -278,8 +312,8 @@
         StatusBarNotification[] notifications =
                 context.getSystemService(NotificationManager.class).getActiveNotifications();
         for (StatusBarNotification statusBarNotification : notifications) {
-            OngoingActivityData data = OngoingActivityData.create(
-                    statusBarNotification.getNotification());
+            OngoingActivityData data =
+                    OngoingActivityData.create(statusBarNotification.getNotification());
             if (data != null && filter.test(data)) {
                 return new OngoingActivity(statusBarNotification.getId(),
                         new NotificationCompat.Builder(context,
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java
index 17130b4..5b07295 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java
@@ -18,6 +18,7 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.graphics.drawable.Icon;
+import android.os.SystemClock;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -35,34 +36,47 @@
 public class OngoingActivityData implements VersionedParcelable {
     @Nullable
     @ParcelField(value = 1, defaultValue = "null")
-    private final Icon mAnimatedIcon;
+    Icon mAnimatedIcon;
 
     @NonNull
     @ParcelField(value = 2)
-    private final Icon mStaticIcon;
+    Icon mStaticIcon;
 
     @Nullable
     @ParcelField(value = 3, defaultValue = "null")
-    private OngoingActivityStatus mStatus;
+    OngoingActivityStatus mStatus;
 
     @NonNull
     @ParcelField(value = 4)
-    private final PendingIntent mTouchIntent;
+    PendingIntent mTouchIntent;
 
     @Nullable
     @ParcelField(value = 5, defaultValue = "null")
-    private final LocusIdCompat mLocusId;
+    String mLocusId;
 
     @ParcelField(value = 6, defaultValue = "-1")
-    private final int mOngoingActivityId;
+    int mOngoingActivityId;
+
+    @Nullable
+    @ParcelField(value = 7, defaultValue = "null")
+    String mCategory;
+
+    @ParcelField(value = 8)
+    long mTimestamp;
+
+    // Required by VersionedParcelable
+    OngoingActivityData() {
+    }
 
     OngoingActivityData(
             @Nullable Icon animatedIcon,
             @NonNull Icon staticIcon,
             @Nullable OngoingActivityStatus status,
             @NonNull PendingIntent touchIntent,
-            @Nullable LocusIdCompat locusId,
-            int ongoingActivityId
+            @Nullable String locusId,
+            int ongoingActivityId,
+            @Nullable String category,
+            long timestamp
     ) {
         mAnimatedIcon = animatedIcon;
         mStaticIcon = staticIcon;
@@ -70,6 +84,8 @@
         mTouchIntent = touchIntent;
         mLocusId = locusId;
         mOngoingActivityId = ongoingActivityId;
+        mCategory = category;
+        mTimestamp = timestamp;
     }
 
     @NonNull
@@ -122,7 +138,8 @@
 
     /**
      * Get the static icon that can be used on some surfaces to represent this
-     * {@link OngoingActivity}. For example in the WatchFace in ambient mode.
+     * {@link OngoingActivity}. For example in the WatchFace in ambient mode. If not set, returns
+     *  the small icon of the corresponding Notification.
      */
     @NonNull
     public Icon getStaticIcon() {
@@ -131,7 +148,8 @@
 
     /**
      * Get the status of this ongoing activity, the status may be displayed on the UI to
-     * show progress of the Ongoing Activity.
+     * show progress of the Ongoing Activity. If not set, returns the content text of the
+     * corresponding Notification.
      */
     @Nullable
     public OngoingActivityStatus getStatus() {
@@ -140,7 +158,8 @@
 
     /**
      * Get the intent to be used to go back to the activity when the user interacts with the
-     * Ongoing Activity in other surfaces (for example, taps the Icon on the WatchFace)
+     * Ongoing Activity in other surfaces (for example, taps the Icon on the WatchFace). If not
+     * set, returns the touch intent of the corresponding Notification.
      */
     @NonNull
     public PendingIntent getTouchIntent() {
@@ -149,11 +168,12 @@
 
     /**
      * Get the LocusId of this {@link OngoingActivity}, this can be used by the launcher to
-     * identify the corresponding launcher item and display it accordingly.
+     * identify the corresponding launcher item and display it accordingly. If not set, returns
+     * the one in the corresponding Notification.
      */
     @Nullable
     public LocusIdCompat getLocusId() {
-        return mLocusId;
+        return new LocusIdCompat(mLocusId);
     }
 
     /**
@@ -164,6 +184,22 @@
         return mOngoingActivityId;
     }
 
+    /**
+     * Get the Category of this {@link OngoingActivity} if set, otherwise the category of the
+     * corresponding notification.
+     */
+    @Nullable
+    public String getCategory() {
+        return mCategory;
+    }
+
+    /**
+     * Get the time (in {@link SystemClock#elapsedRealtime()} time) the OngoingActivity was built.
+     */
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
     // Status is mutable, by the library.
     void setStatus(@NonNull OngoingActivityStatus status) {
         mStatus = status;
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java
index fc37c9d..2dc53a7 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java
@@ -21,6 +21,8 @@
 import androidx.versionedparcelable.VersionedParcelable;
 import androidx.versionedparcelable.VersionedParcelize;
 
+import kotlin.NotImplementedError;
+
 /**
  * Base class to serialize / deserialize {@link OngoingActivityStatus} into / from a Notification
  *
@@ -29,7 +31,12 @@
  * {@link android.os.SystemClock#elapsedRealtime()}
  */
 @VersionedParcelize
-public abstract class OngoingActivityStatus implements VersionedParcelable {
+public class OngoingActivityStatus implements VersionedParcelable {
+
+    // Required by VersionedParcelable
+    OngoingActivityStatus() {
+    }
+
     /**
      * Returns a textual representation of the ongoing activity status at the given time
      * represented as milliseconds timestamp
@@ -43,7 +50,9 @@
      *                      returned by {@link android.os.SystemClock#elapsedRealtime()}.
      */
     @NonNull
-    public abstract CharSequence getText(@NonNull Context context, long timeNowMillis);
+    public CharSequence getText(@NonNull Context context, long timeNowMillis) {
+        throw new NotImplementedError();
+    }
 
     /**
      * Returns the timestamp of the next time when the display will be different from the current
@@ -56,7 +65,9 @@
      * @return the first point in time after {@code fromTimeMillis} when the displayed value of
      * this status will change. returns Long.MAX_VALUE if the display will never change.
      */
-    public abstract long getNextChangeTimeMillis(long fromTimeMillis);
+    public long getNextChangeTimeMillis(long fromTimeMillis) {
+        throw new NotImplementedError();
+    }
 
     // Invalid value to use for paused_at and duration, as suggested by api guidelines 5.15
     static final long LONG_DEFAULT = -1L;
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java
index bceaded..d31e814 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java
@@ -31,7 +31,11 @@
 public class TextOngoingActivityStatus extends OngoingActivityStatus {
     @NonNull
     @ParcelField(value = 1, defaultValue = "")
-    private String mStr = "";
+    String mStr = "";
+
+    // Required by VersionedParcelable
+    TextOngoingActivityStatus() {
+    }
 
     public TextOngoingActivityStatus(@NonNull String str) {
         this.mStr = str;
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java
index 524423e..79d76df 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java
@@ -20,6 +20,7 @@
 import android.text.format.DateUtils;
 
 import androidx.annotation.NonNull;
+import androidx.versionedparcelable.NonParcelField;
 import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.VersionedParcelize;
 
@@ -31,20 +32,26 @@
 @VersionedParcelize
 public class TimerOngoingActivityStatus extends OngoingActivityStatus {
     @ParcelField(value = 1, defaultValue = "0")
-    private long mTimeZeroMillis;
+    long mTimeZeroMillis;
 
     @ParcelField(value = 2, defaultValue = "false")
-    private boolean mCountDown = false;
+    boolean mCountDown = false;
 
     @ParcelField(value = 3, defaultValue = "-1")
-    private long mPausedAtMillis = LONG_DEFAULT;
+    long mPausedAtMillis = LONG_DEFAULT;
 
     @ParcelField(value = 4, defaultValue = "-1")
-    private long mTotalDurationMillis = LONG_DEFAULT;
+    long mTotalDurationMillis = LONG_DEFAULT;
 
+    @NonParcelField
     private final StringBuilder mStringBuilder = new StringBuilder(8);
+
     private static final String NEGATIVE_DURATION_PREFIX = "-";
 
+    // Required by VersionedParcelable
+    TimerOngoingActivityStatus() {
+    }
+
     /**
      * Create a Status representing a timer or stopwatch.
      *
diff --git a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
index 2a78640..7648800 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
@@ -25,6 +25,8 @@
 import androidx.annotation.UiThread;
 import androidx.wear.utils.ActivityAnimationUtil;
 
+import org.jetbrains.annotations.NotNull;
+
 /**
  * Controller that handles the back button click for dismiss the frame layout
  *
@@ -37,22 +39,26 @@
     BackButtonDismissController(Context context, DismissibleFrameLayout layout) {
         super(context, layout);
 
-        // Dismiss upon back button press
+        // set this to true will also ensure that this view is focusable
         layout.setFocusableInTouchMode(true);
+        // Dismiss upon back button press
         layout.requestFocus();
         layout.setOnKeyListener(
-                (view, keyCode, event) -> {
-                    if (keyCode == KeyEvent.KEYCODE_BACK
-                            && event.getAction() == KeyEvent.ACTION_UP) {
-                        dismiss();
-                        return true;
-                    }
-                    return false;
-                });
+                (view, keyCode, event) -> keyCode == KeyEvent.KEYCODE_BACK
+                        && event.getAction() == KeyEvent.ACTION_UP
+                        && dismiss());
     }
 
-    private void dismiss() {
-        if (mDismissListener == null) return;
+    void disable(@NotNull DismissibleFrameLayout layout) {
+        setOnDismissListener(null);
+        layout.setOnKeyListener(null);
+        // setting this to false will also ensure that this view is not focusable in touch mode
+        layout.setFocusable(false);
+        layout.clearFocus();
+    }
+
+    private boolean dismiss() {
+        if (mDismissListener == null) return false;
 
         Animation exitAnimation = ActivityAnimationUtil.getStandardActivityAnimation(
                 mContext, ActivityAnimationUtil.CLOSE_EXIT,
@@ -79,5 +85,6 @@
             mDismissListener.onDismissStarted();
             mDismissListener.onDismissed();
         }
+        return true;
     }
 }
diff --git a/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java b/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
index ce76639..448df50 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
@@ -192,7 +192,7 @@
                 mBackButtonDismissController.setOnDismissListener(mDismissListener);
             }
         } else if (mBackButtonDismissController != null) {
-            mBackButtonDismissController.setOnDismissListener(null);
+            mBackButtonDismissController.disable(this);
             mBackButtonDismissController = null;
         }
     }
diff --git a/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java b/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
index 4535a4c..3a5cc6c 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
@@ -92,17 +92,8 @@
 
         /**
          * Check whether the widget contains invalid attributes as a child of WearArcLayout
-         *
-         * @param clockwise the layout direction of the container
          */
-        void checkInvalidAttributeAsChild(boolean clockwise);
-
-        /**
-         * Return whether the widget will handle the layout rotation requested by the container
-         * If return true, make sure that the layout rotation is done inside the widget since the
-         * container will skip this process.
-         */
-        boolean handleLayoutRotate(float angle);
+        void checkInvalidAttributeAsChild();
 
         /**
          * Return true when the given point is in the clickable area of the child widget.
@@ -120,7 +111,7 @@
      *
      * <p>Note that the {@code rotate} parameter is ignored when drawing "Fullscreen" elements.
      */
-    public static class LayoutParams extends ViewGroup.LayoutParams {
+    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
 
         /** Vertical alignment of elements within the arc. */
         /** @hide */
@@ -265,6 +256,8 @@
     // Temporary variables using during a draw cycle.
     private float mCurrentCumulativeAngle = 0;
     private int mAnglesIndex = 0;
+    @SuppressWarnings("SyntheticAccessor")
+    private final ChildArcAngles mChildArcAngles = new ChildArcAngles();
 
     public WearArcLayout(@NonNull Context context) {
         this(context, null);
@@ -358,18 +351,22 @@
 
             // ArcLayoutWidget is a special case. Because of how it draws, fit it to the size
             // of the whole widget.
+            int childMeasuredHeight;
             if (child instanceof ArcLayoutWidget) {
-                ArcLayoutWidget widget = (ArcLayoutWidget) child;
-                maxChildHeightPx = max(maxChildHeightPx, widget.getThicknessPx());
+                childMeasuredHeight = ((ArcLayoutWidget) child).getThicknessPx();
             } else {
                 measureChild(
                         child,
                         getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().width),
                         getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().height)
                 );
-                maxChildHeightPx = max(maxChildHeightPx, child.getMeasuredHeight());
+                childMeasuredHeight = child.getMeasuredHeight();
                 childState = combineMeasuredStates(childState, child.getMeasuredState());
+
             }
+            LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
+            maxChildHeightPx = max(maxChildHeightPx, childMeasuredHeight
+                    + childLayoutParams.topMargin +  childLayoutParams.bottomMargin);
         }
 
         mThicknessPx = maxChildHeightPx;
@@ -383,23 +380,14 @@
             }
 
             if (child instanceof ArcLayoutWidget) {
-                ArcLayoutWidget curvedContainerChild = (ArcLayoutWidget) child;
                 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
 
-                int childThicknessPx = curvedContainerChild.getThicknessPx();
-                int thicknessDiffPx = mThicknessPx - childThicknessPx;
-
-                float insetPx = 0;
-
-                if (childLayoutParams.getVerticalAlignment() == LayoutParams.VALIGN_CENTER) {
-                    insetPx = thicknessDiffPx / 2f;
-                } else if (childLayoutParams.getVerticalAlignment() == LayoutParams.VALIGN_INNER) {
-                    insetPx = thicknessDiffPx;
-                }
+                float insetPx = getChildTopInset(child);
 
                 int innerChildMeasureSpec =
                         MeasureSpec.makeMeasureSpec(
                                 maxChildDimension * 2 - round(insetPx * 2), MeasureSpec.EXACTLY);
+
                 measureChild(
                         child,
                         getChildMeasureSpec(innerChildMeasureSpec, 0, childLayoutParams.width),
@@ -444,7 +432,7 @@
                 // vertical position.
                 int leftPx =
                         round((getMeasuredWidth() / 2f) - (child.getMeasuredWidth() / 2f));
-                int topPx = getChildTopInset(child);
+                int topPx = round(getChildTopInset(child));
 
                 child.layout(
                         leftPx,
@@ -551,60 +539,49 @@
         // Rotate the canvas to make the children render in the right place.
         canvas.save();
 
-        float arcAngle = calculateArcAngle(child);
-        float preRotation = arcAngle / 2f;
+        calculateArcAngle(child, mChildArcAngles);
+        float preRotation = mChildArcAngles.leftMarginAsAngle
+                + mChildArcAngles.actualChildAngle / 2f;
         float multiplier = mClockwise ? 1f : -1f;
 
         // Store the center angle of each child to handle touch events.
-        float middleAngle = multiplier * (mCurrentCumulativeAngle + arcAngle / 2);
+        float middleAngle = multiplier * (mCurrentCumulativeAngle + preRotation);
         mAngles[mAnglesIndex++] = middleAngle;
         if (child == mTouchedView) {
             // We keep this updated, in case the view has changed angle.
             mTouchedViewAngle = middleAngle;
         }
 
+        // Rotate the child widget.
+        canvas.rotate(
+                multiplier * (mCurrentCumulativeAngle + preRotation),
+                getMeasuredWidth() / 2f,
+                getMeasuredHeight() / 2f);
+
         if (child instanceof ArcLayoutWidget) {
-            ArcLayoutWidget childWidget = (ArcLayoutWidget) child;
-            childWidget.checkInvalidAttributeAsChild(mClockwise);
-
-            // Special case for ArcLayoutWidget. This doesn't need pre-rotating to get the center
-            // of canvas lines up, as it should already know how to draw itself correctly from
-            // the "current" rotation. The layout rotation is always passed to the child widget,
-            // if the child has not handled this rotation by itself, the parent will have to
-            // rotate the canvas to apply this layout.
-            if (!childWidget.handleLayoutRotate(multiplier * mCurrentCumulativeAngle)) {
-                canvas.rotate(
-                        multiplier * mCurrentCumulativeAngle,
-                        getMeasuredWidth() / 2f,
-                        getMeasuredHeight() / 2f
-                );
-            }
+            ((ArcLayoutWidget) child).checkInvalidAttributeAsChild();
         } else {
-            canvas.rotate(
-                    multiplier * (mCurrentCumulativeAngle + preRotation),
-                    getMeasuredWidth() / 2f,
-                    getMeasuredHeight() / 2f);
-
             // Do we need to do some counter rotation?
             LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
 
-            // For counterclockwise layout, especially when mixing standard Android widget with
-            // ArcLayoutWidget as children, we might need to rotate the standard widget to make
-            // them with the same upwards direction.
             float angleToRotate = 0f;
-            if (layoutParams.getRotate() && !mClockwise) {
-                angleToRotate = 180f;
-            }
 
-            // Un-rotate about the top of the canvas, around the center of the actual child.
-            // This compounds with the initial rotation into a translation.
-            if (!layoutParams.getRotate()) {
+            if (layoutParams.getRotate()) {
+                // For counterclockwise layout, especially when mixing standard Android widget with
+                // ArcLayoutWidget as children, we might need to rotate the standard widget to make
+                // them have the same upwards direction.
+                if (!mClockwise) {
+                    angleToRotate = 180f;
+                }
+            } else {
+                // Un-rotate about the top of the canvas, around the center of the actual child.
+                // This compounds with the initial rotation into a translation.
                 angleToRotate = -multiplier * (mCurrentCumulativeAngle + preRotation);
             }
 
             // Do the actual rotation. Note that the strange rotation center is because the child
             // view is x-centered but at the top of this container.
-            int childInset = getChildTopInset(child);
+            float childInset = getChildTopInset(child);
             canvas.rotate(
                     angleToRotate,
                     getMeasuredWidth() / 2f,
@@ -612,7 +589,7 @@
             );
         }
 
-        mCurrentCumulativeAngle += arcAngle;
+        mCurrentCumulativeAngle += mChildArcAngles.getTotalAngle();
 
         boolean wasInvalidateIssued = super.drawChild(canvas, child, drawingTime);
 
@@ -630,7 +607,8 @@
         float totalArcAngle = 0;
 
         for (int i = 0; i < getChildCount(); i++) {
-            totalArcAngle += calculateArcAngle(getChildAt(i));
+            calculateArcAngle(getChildAt(i), mChildArcAngles);
+            totalArcAngle += mChildArcAngles.getTotalAngle();
         }
 
         if (mAnchorType == ANCHOR_CENTER) {
@@ -642,31 +620,55 @@
         return 0;
     }
 
-    private float calculateArcAngle(@NonNull View view) {
+    private static float widthToAngleDegrees(float widthPx, float radiusPx) {
+        return (float) Math.toDegrees(2 * asin(widthPx / radiusPx / 2f));
+    }
+
+    private void calculateArcAngle(@NonNull View view, @NonNull ChildArcAngles childAngles) {
         if (view.getVisibility() == GONE) {
-            return 0f;
+            childAngles.leftMarginAsAngle = 0;
+            childAngles.rightMarginAsAngle = 0;
+            childAngles.actualChildAngle = 0;
+            return;
         }
 
+        float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
+
+        LayoutParams childLayoutParams = (LayoutParams) view.getLayoutParams();
+
+        childAngles.leftMarginAsAngle =
+                widthToAngleDegrees(childLayoutParams.leftMargin, radiusPx);
+        childAngles.rightMarginAsAngle =
+                widthToAngleDegrees(childLayoutParams.rightMargin, radiusPx);
+
         if (view instanceof ArcLayoutWidget) {
-            return ((ArcLayoutWidget) view).getSweepAngleDegrees();
+            childAngles.actualChildAngle = ((ArcLayoutWidget) view).getSweepAngleDegrees();
         } else {
-            float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
-            return (float) Math.toDegrees(2 * asin(view.getMeasuredWidth() / radiusPx / 2f));
+            childAngles.actualChildAngle =
+                    widthToAngleDegrees(view.getMeasuredWidth(), radiusPx);
         }
     }
 
-    private int getChildTopInset(@NonNull View child) {
+    private float getChildTopInset(@NonNull View child) {
         LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
 
-        int thicknessDiffPx = mThicknessPx - child.getMeasuredHeight();
+        int childHeight = child instanceof ArcLayoutWidget
+                ? ((ArcLayoutWidget) child).getThicknessPx()
+                : child.getMeasuredHeight();
+
+        int thicknessDiffPx =
+                mThicknessPx - childLayoutParams.topMargin - childLayoutParams.bottomMargin
+                        - childHeight;
+
+        int margin = mClockwise ? childLayoutParams.topMargin : childLayoutParams.bottomMargin;
 
         switch (childLayoutParams.getVerticalAlignment()) {
             case LayoutParams.VALIGN_OUTER:
-                return 0;
+                return margin;
             case LayoutParams.VALIGN_CENTER:
-                return round(thicknessDiffPx / 2f);
+                return margin + thicknessDiffPx / 2f;
             case LayoutParams.VALIGN_INNER:
-                return thicknessDiffPx;
+                return margin + thicknessDiffPx;
             default:
                 // Nortmally unreachable...
                 return 0;
@@ -734,4 +736,14 @@
         mClockwise = clockwise;
         invalidate();
     }
+
+    private static class ChildArcAngles {
+        public float leftMarginAsAngle;
+        public float rightMarginAsAngle;
+        public float actualChildAngle;
+
+        public float getTotalAngle() {
+            return leftMarginAsAngle + rightMarginAsAngle + actualChildAngle;
+        }
+    }
 }
diff --git a/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java b/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
index 5a6a7fb..b2e659e 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
@@ -77,12 +77,8 @@
     private float mPathRadius = 0f;
     private float mTextSweepDegrees = 0f;
     private float mBackgroundSweepDegrees = MAX_SWEEP_DEGREE;
-    private boolean mHasParentArcLayout = false;
-    private boolean mParentClockwise = true;
     private int mLastUsedTextAlignment = -1;
     private float mLocalRotateAngle = 0f;
-    private float mParentRotateAngle = 0f;
-    private boolean mParentRotateAngleSet = false;
 
     private int mAnchorType = UNSET_ANCHOR_TYPE;
     private float mAnchorAngleDegrees = UNSET_ANCHOR_DEGREE;
@@ -215,10 +211,7 @@
      *                                  were set for a widget in WearArcLayout
      */
     @Override
-    public void checkInvalidAttributeAsChild(boolean parentClockwise) {
-        this.mHasParentArcLayout = true;
-        this.mParentClockwise = parentClockwise;
-
+    public void checkInvalidAttributeAsChild() {
         if (mAnchorType != UNSET_ANCHOR_TYPE) {
             throw new IllegalArgumentException(
                     "WearCurvedTextView shall not set anchorType value when added into"
@@ -235,18 +228,6 @@
     }
 
     @Override
-    public boolean handleLayoutRotate(float angle) {
-        mParentRotateAngleSet = true;
-
-        // Ensure we are redrawn when the parent rotates.
-        if (mParentRotateAngle != angle) {
-            doRedraw();
-        }
-        mParentRotateAngle = angle;
-        return true;
-    }
-
-    @Override
     public boolean insideClickArea(float x, float y) {
         float radius2 = min(getWidth(), getHeight()) / 2f
                 - (mClockwise ? getPaddingTop() : getPaddingBottom());
@@ -340,8 +321,6 @@
         }
 
         float clockwiseFactor = mClockwise ? 1f : -1f;
-        float parentClockwiseFactor =
-                mHasParentArcLayout ? (mParentClockwise ? 1f : -1f) : clockwiseFactor;
 
         float alignmentFactor = 0.5f;
         switch (getTextAlignment()) {
@@ -357,31 +336,25 @@
                 alignmentFactor = 0.5f; // TEXT_ALIGNMENT_CENTER
         }
 
-        float anchorTypeFactor = 0f;
+        float anchorTypeFactor;
         switch (mAnchorType) {
             case WearArcLayout.ANCHOR_START:
-                anchorTypeFactor = 0f;
-                break;
-            case WearArcLayout.ANCHOR_CENTER:
                 anchorTypeFactor = 0.5f;
                 break;
             case WearArcLayout.ANCHOR_END:
-                anchorTypeFactor = 1f;
+                anchorTypeFactor = -0.5f;
                 break;
+            case WearArcLayout.ANCHOR_CENTER: // Center is the default.
             default:
-                anchorTypeFactor = parentClockwiseFactor == clockwiseFactor ? 0f : -1f;
+                anchorTypeFactor = 0f;
         }
 
-        float actualAnchorDegree =
-                (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
-                        + ANCHOR_DEGREE_OFFSET;
+        mLocalRotateAngle = (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
+                + clockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;
 
         // Always draw the curved text on top center, then rotate the canvas to the right position
         float backgroundStartAngle =
                 -clockwiseFactor * 0.5f * mBackgroundSweepDegrees + ANCHOR_DEGREE_OFFSET;
-        mLocalRotateAngle =
-                actualAnchorDegree - backgroundStartAngle
-                        - parentClockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;
 
         float textStartAngle =
                 backgroundStartAngle + clockwiseFactor * (float) (
@@ -462,20 +435,15 @@
             return false;
         }
 
-        float x0 = event.getX();
-        float y0 = event.getY();
-        if (!mParentRotateAngleSet) {
-            // If we are a stand-alone widget, we have to handle our rotation / anchor placement,
-            // if we are part of an arc container, it's handled by it.
-            double rotAngle = -Math.toRadians(mLocalRotateAngle);
+        float x0 = event.getX() - getWidth() / 2;
+        float y0 = event.getY() - getHeight() / 2;
 
-            x0 -= getWidth() / 2;
-            y0 -= getHeight() / 2;
-            float tempX = (float)
-                    ((x0 * cos(rotAngle) - y0 * sin(rotAngle)) + getWidth() / 2);
-            y0 = (float) ((x0 * sin(rotAngle) + y0 * cos(rotAngle)) + getHeight() / 2);
-            x0 = tempX;
-        }
+        double rotAngle = -Math.toRadians(mLocalRotateAngle);
+
+        float tempX = (float)
+                ((x0 * cos(rotAngle) - y0 * sin(rotAngle)) + getWidth() / 2);
+        y0 = (float) ((x0 * sin(rotAngle) + y0 * cos(rotAngle)) + getHeight() / 2);
+        x0 = tempX;
 
         // Should we start handling the touch events?
         if (!mHandlingTouch && insideClickArea(x0, y0)) {
@@ -503,7 +471,7 @@
         boolean withBackground = getBackground() != null;
         updatePathsIfNeeded(withBackground);
         canvas.rotate(
-                mLocalRotateAngle + mParentRotateAngle,
+                mLocalRotateAngle,
                 getWidth() / 2f,
                 getHeight() / 2f);
 
diff --git a/wear/wear/src/main/java/androidx/wear/widget/WearableRecyclerView.java b/wear/wear/src/main/java/androidx/wear/widget/WearableRecyclerView.java
index 07da857..a81a4d7c 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/WearableRecyclerView.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/WearableRecyclerView.java
@@ -232,15 +232,20 @@
     }
 
     /**
-     * Use this method to configure the {@link WearableRecyclerView} to always align the first and
-     * last items with the vertical center of the screen. This effectively moves the start and end
-     * of the list to the middle of the screen if the user has scrolled so far. It takes the height
-     * of the children into account so that they are correctly centered.
+     * Use this method to configure the {@link WearableRecyclerView} on round watches to always
+     * align the first and last items with the vertical center of the screen. This effectively moves
+     * the start and end of the list to the middle of the screen if the user has scrolled so far.
+     * It takes the height of the children into account so that they are correctly centered.
+     * On nonRound watches, this method has no effect and original padding is used.
      *
      * @param isEnabled set to true if you wish to align the edge children (first and last)
      *                        with the center of the screen.
      */
     public void setEdgeItemsCenteringEnabled(boolean isEnabled) {
+        if (!getResources().getConfiguration().isScreenRound()) {
+            mEdgeItemsCenteringEnabled = false;
+            return;
+        }
         mEdgeItemsCenteringEnabled = isEnabled;
         if (mEdgeItemsCenteringEnabled) {
             if (getChildCount() > 0) {
diff --git a/window/window-extensions/api/current.txt b/window/window-extensions/api/current.txt
index 01608ca..78bdc7f 100644
--- a/window/window-extensions/api/current.txt
+++ b/window/window-extensions/api/current.txt
@@ -11,10 +11,18 @@
     field public static final int POSTURE_UNKNOWN = 0; // 0x0
   }
 
-  public class ExtensionDisplayFeature {
-    ctor public ExtensionDisplayFeature(android.graphics.Rect, int);
+  public interface ExtensionDisplayFeature {
     method public android.graphics.Rect getBounds();
+  }
+
+  public class ExtensionFoldingFeature implements androidx.window.extensions.ExtensionDisplayFeature {
+    ctor public ExtensionFoldingFeature(android.graphics.Rect, int, int);
+    method public android.graphics.Rect getBounds();
+    method public int getState();
     method public int getType();
+    field public static final int STATE_FLAT = 1; // 0x1
+    field public static final int STATE_FLIPPED = 3; // 0x3
+    field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
diff --git a/window/window-extensions/api/public_plus_experimental_current.txt b/window/window-extensions/api/public_plus_experimental_current.txt
index 01608ca..78bdc7f 100644
--- a/window/window-extensions/api/public_plus_experimental_current.txt
+++ b/window/window-extensions/api/public_plus_experimental_current.txt
@@ -11,10 +11,18 @@
     field public static final int POSTURE_UNKNOWN = 0; // 0x0
   }
 
-  public class ExtensionDisplayFeature {
-    ctor public ExtensionDisplayFeature(android.graphics.Rect, int);
+  public interface ExtensionDisplayFeature {
     method public android.graphics.Rect getBounds();
+  }
+
+  public class ExtensionFoldingFeature implements androidx.window.extensions.ExtensionDisplayFeature {
+    ctor public ExtensionFoldingFeature(android.graphics.Rect, int, int);
+    method public android.graphics.Rect getBounds();
+    method public int getState();
     method public int getType();
+    field public static final int STATE_FLAT = 1; // 0x1
+    field public static final int STATE_FLIPPED = 3; // 0x3
+    field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
diff --git a/window/window-extensions/api/restricted_current.txt b/window/window-extensions/api/restricted_current.txt
index 01608ca..78bdc7f 100644
--- a/window/window-extensions/api/restricted_current.txt
+++ b/window/window-extensions/api/restricted_current.txt
@@ -11,10 +11,18 @@
     field public static final int POSTURE_UNKNOWN = 0; // 0x0
   }
 
-  public class ExtensionDisplayFeature {
-    ctor public ExtensionDisplayFeature(android.graphics.Rect, int);
+  public interface ExtensionDisplayFeature {
     method public android.graphics.Rect getBounds();
+  }
+
+  public class ExtensionFoldingFeature implements androidx.window.extensions.ExtensionDisplayFeature {
+    ctor public ExtensionFoldingFeature(android.graphics.Rect, int, int);
+    method public android.graphics.Rect getBounds();
+    method public int getState();
     method public int getType();
+    field public static final int STATE_FLAT = 1; // 0x1
+    field public static final int STATE_FLIPPED = 3; // 0x3
+    field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
diff --git a/window/window-extensions/build.gradle b/window/window-extensions/build.gradle
index 82c95ec..6e84dbc 100644
--- a/window/window-extensions/build.gradle
+++ b/window/window-extensions/build.gradle
@@ -19,6 +19,13 @@
 import androidx.build.Publish
 import androidx.build.RunApiTasks
 
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_EXT_JUNIT
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RULES
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RUNNER
+import static androidx.build.dependencies.DependenciesKt.DEXMAKER_MOCKITO
+import static androidx.build.dependencies.DependenciesKt.MOCKITO_CORE
+import static androidx.build.dependencies.DependenciesKt.TRUTH
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -32,6 +39,12 @@
 
 dependencies {
     implementation("androidx.annotation:annotation:1.1.0")
+
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy)
 }
 
 androidx {
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/window/window-extensions/src/androidTest/AndroidManifest.xml
similarity index 68%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to window/window-extensions/src/androidTest/AndroidManifest.xml
index 3bc2684..d85d231 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/window/window-extensions/src/androidTest/AndroidManifest.xml
@@ -15,5 +15,11 @@
   limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.car.app">
+    package="androidx.window.extensions.test">
+
+    <application>
+        <activity android:name="androidx.window.extensions.TestActivity" />
+        <activity android:name="androidx.window.extensions.TestConfigChangeHandlingActivity"
+            android:configChanges="orientation|screenLayout|screenSize"/>
+    </application>
 </manifest>
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDeviceStateTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDeviceStateTest.java
new file mode 100644
index 0000000..3299a08
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDeviceStateTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ExtensionDeviceState} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ExtensionDeviceStateTest {
+
+    @Test
+    public void testEquals_samePosture() {
+        ExtensionDeviceState original = new ExtensionDeviceState(0);
+        ExtensionDeviceState copy = new ExtensionDeviceState(0);
+
+        assertEquals(original, copy);
+    }
+
+    @Test
+    public void testEquals_differentPosture() {
+        ExtensionDeviceState original = new ExtensionDeviceState(0);
+        ExtensionDeviceState different = new ExtensionDeviceState(1);
+
+        assertNotEquals(original, different);
+    }
+
+    @Test
+    public void testHashCode_matchesIfEqual() {
+        int posture = 111;
+        ExtensionDeviceState original = new ExtensionDeviceState(posture);
+        ExtensionDeviceState matching = new ExtensionDeviceState(posture);
+
+        assertEquals(original, matching);
+        assertEquals(original.hashCode(), matching.hashCode());
+    }
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
new file mode 100644
index 0000000..fdd42fa
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ExtensionFoldingFeature} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ExtensionDisplayFeatureTest {
+
+    @Test
+    public void testEquals_sameAttributes() {
+        Rect bounds = new Rect(1, 0, 1, 10);
+        int type = ExtensionFoldingFeature.TYPE_FOLD;
+        int state = ExtensionFoldingFeature.STATE_FLAT;
+
+        ExtensionFoldingFeature original = new ExtensionFoldingFeature(bounds, type, state);
+        ExtensionFoldingFeature copy = new ExtensionFoldingFeature(bounds, type, state);
+
+        assertEquals(original, copy);
+    }
+
+    @Test
+    public void testEquals_differentRect() {
+        Rect originalRect = new Rect(1, 0, 1, 10);
+        Rect otherRect = new Rect(2, 0, 2, 10);
+        int type = ExtensionFoldingFeature.TYPE_FOLD;
+        int state = ExtensionFoldingFeature.STATE_FLAT;
+
+        ExtensionFoldingFeature original = new ExtensionFoldingFeature(originalRect, type,
+                state);
+        ExtensionFoldingFeature other = new ExtensionFoldingFeature(otherRect, type, state);
+
+        assertNotEquals(original, other);
+    }
+
+    @Test
+    public void testEquals_differentType() {
+        Rect rect = new Rect(1, 0, 1, 10);
+        int originalType = ExtensionFoldingFeature.TYPE_FOLD;
+        int otherType = ExtensionFoldingFeature.TYPE_HINGE;
+        int state = ExtensionFoldingFeature.STATE_FLAT;
+
+        ExtensionFoldingFeature original = new ExtensionFoldingFeature(rect, originalType,
+                state);
+        ExtensionFoldingFeature other = new ExtensionFoldingFeature(rect, otherType, state);
+
+        assertNotEquals(original, other);
+    }
+
+    @Test
+    public void testEquals_differentState() {
+        Rect rect = new Rect(1, 0, 1, 10);
+        int type = ExtensionFoldingFeature.TYPE_FOLD;
+        int originalState = ExtensionFoldingFeature.STATE_FLAT;
+        int otherState = ExtensionFoldingFeature.STATE_FLIPPED;
+
+        ExtensionFoldingFeature original = new ExtensionFoldingFeature(rect, type,
+                originalState);
+        ExtensionFoldingFeature other = new ExtensionFoldingFeature(rect, type, otherState);
+
+        assertNotEquals(original, other);
+    }
+
+    @Test
+    public void testHashCode_matchesIfEqual() {
+        Rect originalRect = new Rect(1, 0, 1, 10);
+        Rect matchingRect = new Rect(1, 0, 1, 10);
+        int type = ExtensionFoldingFeature.TYPE_FOLD;
+        int state = ExtensionFoldingFeature.STATE_FLAT;
+
+        ExtensionFoldingFeature original = new ExtensionFoldingFeature(originalRect, type,
+                state);
+        ExtensionFoldingFeature matching = new ExtensionFoldingFeature(matchingRect, type,
+                state);
+
+        assertEquals(original, matching);
+        assertEquals(original.hashCode(), matching.hashCode());
+    }
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionWindowLayoutInfoTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionWindowLayoutInfoTest.java
new file mode 100644
index 0000000..bb87c3b
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionWindowLayoutInfoTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link ExtensionWindowLayoutInfo} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ExtensionWindowLayoutInfoTest {
+
+    @Test
+    public void testEquals_sameFeatures() {
+        List<ExtensionDisplayFeature> features = new ArrayList<>();
+
+        ExtensionWindowLayoutInfo original = new ExtensionWindowLayoutInfo(features);
+        ExtensionWindowLayoutInfo copy = new ExtensionWindowLayoutInfo(features);
+
+        assertEquals(original, copy);
+    }
+
+    @Test
+    public void testEquals_differentFeatures() {
+        List<ExtensionDisplayFeature> originalFeatures = new ArrayList<>();
+        List<ExtensionDisplayFeature> differentFeatures = new ArrayList<>();
+        Rect rect = new Rect(1, 0, 1, 10);
+        differentFeatures.add(new ExtensionFoldingFeature(
+                rect, ExtensionFoldingFeature.TYPE_HINGE,
+                ExtensionFoldingFeature.STATE_FLAT));
+
+        ExtensionWindowLayoutInfo original = new ExtensionWindowLayoutInfo(originalFeatures);
+        ExtensionWindowLayoutInfo different = new ExtensionWindowLayoutInfo(differentFeatures);
+
+        assertNotEquals(original, different);
+    }
+
+    @Test
+    public void testHashCode_matchesIfEqual() {
+        List<ExtensionDisplayFeature> firstFeatures = new ArrayList<>();
+        List<ExtensionDisplayFeature> secondFeatures = new ArrayList<>();
+        ExtensionWindowLayoutInfo first = new ExtensionWindowLayoutInfo(firstFeatures);
+        ExtensionWindowLayoutInfo second = new ExtensionWindowLayoutInfo(secondFeatures);
+
+        assertEquals(first, second);
+        assertEquals(first.hashCode(), second.hashCode());
+    }
+
+    @Test
+    public void testHashCode_matchesIfEqualFeatures() {
+        ExtensionDisplayFeature originalFeature = new ExtensionFoldingFeature(
+                new Rect(0, 0, 100, 0),
+                ExtensionFoldingFeature.TYPE_HINGE,
+                ExtensionFoldingFeature.STATE_FLAT
+        );
+        ExtensionDisplayFeature matchingFeature = new ExtensionFoldingFeature(
+                new Rect(0, 0, 100, 0),
+                ExtensionFoldingFeature.TYPE_HINGE,
+                ExtensionFoldingFeature.STATE_FLAT
+        );
+        List<ExtensionDisplayFeature> firstFeatures = Collections.singletonList(originalFeature);
+        List<ExtensionDisplayFeature> secondFeatures = Collections.singletonList(matchingFeature);
+        ExtensionWindowLayoutInfo first = new ExtensionWindowLayoutInfo(firstFeatures);
+        ExtensionWindowLayoutInfo second = new ExtensionWindowLayoutInfo(secondFeatures);
+
+        assertEquals(first, second);
+        assertEquals(first.hashCode(), second.hashCode());
+    }
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestActivity.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestActivity.java
new file mode 100644
index 0000000..1c36baf
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.CountDownLatch;
+
+public class TestActivity extends Activity implements View.OnLayoutChangeListener {
+
+    private int mRootViewId;
+    private CountDownLatch mLayoutLatch = new CountDownLatch(1);
+    private static CountDownLatch sResumeLatch = new CountDownLatch(1);
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final View contentView = new View(this);
+        mRootViewId = View.generateViewId();
+        contentView.setId(mRootViewId);
+        setContentView(contentView);
+
+        getWindow().getDecorView().addOnLayoutChangeListener(this);
+    }
+
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+            int oldTop, int oldRight, int oldBottom) {
+        mLayoutLatch.countDown();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        sResumeLatch.countDown();
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestConfigChangeHandlingActivity.java
similarity index 77%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to window/window-extensions/src/androidTest/java/androidx/window/extensions/TestConfigChangeHandlingActivity.java
index f9cb2fe..f0a2871 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestConfigChangeHandlingActivity.java
@@ -14,7 +14,8 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.focus
+package androidx.window.extensions;
 
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+/** Activity that handles orientation configuration change. */
+public final class TestConfigChangeHandlingActivity extends TestActivity {
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/WindowTestBase.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/WindowTestBase.java
new file mode 100644
index 0000000..b68fc23f
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/WindowTestBase.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import android.app.Activity;
+import android.os.IBinder;
+
+import androidx.test.core.app.ActivityScenario;
+
+import org.junit.Before;
+
+/**
+ * Base class for all tests in the module.
+ */
+class WindowTestBase {
+    ActivityScenario<TestActivity> mActivityTestRule;
+
+    @Before
+    public void setUp() {
+        mActivityTestRule = ActivityScenario.launch(TestActivity.class);
+    }
+
+    static IBinder getActivityWindowToken(Activity activity) {
+        return activity.getWindow().getAttributes().token;
+    }
+}
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java
index be7ce85..f2f1481 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java
@@ -43,8 +43,6 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
-            POSTURE_UNKNOWN,
-            POSTURE_CLOSED,
             POSTURE_HALF_OPENED,
             POSTURE_OPENED,
             POSTURE_FLIPPED
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java
index 8bbd56d..59794d9 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java
@@ -18,110 +18,19 @@
 
 import android.graphics.Rect;
 
-import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 
 /**
  * Description of a physical feature on the display.
  */
-public class ExtensionDisplayFeature {
+public interface ExtensionDisplayFeature {
+
     /**
-     * The bounding rectangle of the feature within the application window in the window
-     * coordinate space.
+     * The bounding rectangle of the feature within the application window
+     * in the window coordinate space.
      *
-     * <p>The bounds for features of type {@link #TYPE_FOLD fold} must be zero-high (for
-     * horizontal folds) or zero-wide (for vertical folds) and span the entire window.
-     *
-     * <p>The bounds for features of type {@link #TYPE_HINGE hinge} must span the entire window
-     * but, unlike folds, can have a non-zero area which represents the region that is occluded by
-     * the hinge and not visible to the user.
+     * @return bounds of display feature.
      */
     @NonNull
-    private final Rect mBounds;
-
-    /**
-     * The physical type of the feature.
-     */
-    @Type
-    private final int mType;
-
-    /**
-     * A fold in the flexible screen without a physical gap.
-     */
-    public static final int TYPE_FOLD = 1;
-
-    /**
-     * A physical separation with a hinge that allows two display panels to fold.
-     */
-    public static final int TYPE_HINGE = 2;
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({
-            TYPE_FOLD,
-            TYPE_HINGE,
-    })
-    @interface Type{}
-
-    public ExtensionDisplayFeature(@NonNull Rect bounds, @Type int type) {
-        mBounds = new Rect(bounds);
-        mType = type;
-    }
-
-    /** Gets the bounding rect of the display feature in window coordinate space. */
-    @NonNull
-    public Rect getBounds() {
-        return new Rect(mBounds);
-    }
-
-    /** Gets the type of the display feature. */
-    @Type
-    public int getType() {
-        return mType;
-    }
-
-    @NonNull
-    private static String typeToString(int type) {
-        switch (type) {
-            case TYPE_FOLD:
-                return "FOLD";
-            case TYPE_HINGE:
-                return "HINGE";
-            default:
-                return "Unknown feature type (" + type + ")";
-        }
-    }
-
-    @NonNull
-    @Override
-    public String toString() {
-        return "ExtensionDisplayFeature { bounds=" + mBounds + ", type=" + typeToString(getType())
-                + " }";
-    }
-
-    @Override
-    public boolean equals(@Nullable Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (!(obj instanceof ExtensionDisplayFeature)) {
-            return false;
-        }
-        final ExtensionDisplayFeature
-                other = (ExtensionDisplayFeature) obj;
-        if (mType != other.mType) {
-            return false;
-        }
-        return mBounds.equals(other.mBounds);
-    }
-
-    @Override
-    public int hashCode() {
-        int result = mType;
-        result = 31 * result + mBounds.centerX() + mBounds.centerY();
-        return result;
-    }
+    Rect getBounds();
 }
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
new file mode 100644
index 0000000..e39e5ca
--- /dev/null
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions;
+
+import android.graphics.Rect;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A feature that describes a fold in a flexible display
+ * or a hinge between two physical display panels.
+ */
+public class ExtensionFoldingFeature implements ExtensionDisplayFeature {
+
+    /**
+     * A fold in the flexible screen without a physical gap.
+     */
+    public static final int TYPE_FOLD = 1;
+
+    /**
+     * A physical separation with a hinge that allows two display panels to fold.
+     */
+    public static final int TYPE_HINGE = 2;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            TYPE_FOLD,
+            TYPE_HINGE,
+    })
+    @interface Type{}
+
+    /**
+     * The foldable device's hinge is completely open, the screen space that is presented to the
+     * user is flat. See the
+     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+     * section in the official documentation for visual samples and references.
+     */
+    public static final int STATE_FLAT = 1;
+
+    /**
+     * The foldable device's hinge is in an intermediate position between opened and closed state,
+     * there is a non-flat angle between parts of the flexible screen or between physical screen
+     * panels. See the
+     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+     * section in the official documentation for visual samples and references.
+     */
+    public static final int STATE_HALF_OPENED = 2;
+
+    /**
+     * The foldable device's hinge is flipped with the flexible screen parts or physical screens
+     * facing opposite directions. See the
+     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+     * section in the official documentation for visual samples and references.
+     */
+    public static final int STATE_FLIPPED = 3;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            STATE_HALF_OPENED,
+            STATE_FLAT,
+            STATE_FLIPPED
+    })
+    @interface State {}
+
+    /**
+     * The bounding rectangle of the feature within the application window in the window
+     * coordinate space.
+     */
+    @NonNull
+    private final Rect mBounds;
+
+    /**
+     * The physical type of the feature.
+     */
+    @Type
+    private final int mType;
+
+    /**
+     * The state of the feature.
+     */
+    @State
+    private final int mState;
+
+    public ExtensionFoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
+        validateFeatureBounds(bounds, type);
+        mBounds = new Rect(bounds);
+        mType = type;
+        mState = state;
+    }
+
+    /** Gets the bounding rect of the display feature in window coordinate space. */
+    @NonNull
+    @Override
+    public Rect getBounds() {
+        return new Rect(mBounds);
+    }
+
+    /** Gets the type of the display feature. */
+    @Type
+    public int getType() {
+        return mType;
+    }
+
+    /** Gets the state of the display feature. */
+    @State
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * Verifies the bounds of the folding feature.
+     */
+    private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+        if (bounds.width() == 0 && bounds.height() == 0) {
+            throw new IllegalArgumentException("Bounds must be non zero");
+        }
+        if (type == TYPE_FOLD) {
+            if (bounds.width() != 0 && bounds.height() != 0) {
+                throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
+                        + "or zero-high for features of type " + typeToString(type));
+            }
+
+            if ((bounds.width() != 0 && bounds.left != 0)
+                    || (bounds.height() != 0 && bounds.top != 0)) {
+                throw new IllegalArgumentException("Bounding rectangle must span the entire "
+                        + "window space for features of type " + typeToString(type));
+            }
+        } else if (type == TYPE_HINGE) {
+            if (bounds.left != 0 && bounds.top != 0) {
+                throw new IllegalArgumentException("Bounding rectangle must span the entire "
+                        + "window space for features of type " + typeToString(type));
+            }
+        }
+    }
+
+    @NonNull
+    private static String typeToString(int type) {
+        switch (type) {
+            case TYPE_FOLD:
+                return "FOLD";
+            case TYPE_HINGE:
+                return "HINGE";
+            default:
+                return "Unknown feature type (" + type + ")";
+        }
+    }
+
+    @NonNull
+    private static String stateToString(int state) {
+        switch (state) {
+            case STATE_FLAT:
+                return "FLAT";
+            case STATE_FLIPPED:
+                return "FLIPPED";
+            case STATE_HALF_OPENED:
+                return "HALF_OPENED";
+            default:
+                return "Unknown feature state (" + state + ")";
+        }
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "ExtensionDisplayFoldFeature { " + mBounds
+                + ", type=" + typeToString(getType()) + ", state=" + stateToString(mState) + " }";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof ExtensionFoldingFeature)) {
+            return false;
+        }
+        final ExtensionFoldingFeature other = (ExtensionFoldingFeature) obj;
+        if (mType != other.mType) {
+            return false;
+        }
+        if (mState != other.mState) {
+            return false;
+        }
+        return mBounds.equals(other.mBounds);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mBounds.hashCode();
+        result = 31 * result + mType;
+        result = 31 * result + mState;
+        return result;
+    }
+}
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java
index 69e7238..0afefc7 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java
@@ -16,8 +16,6 @@
 
 package androidx.window.extensions;
 
-import android.content.Context;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -33,7 +31,6 @@
      * List of display features within the window.
      * <p>NOTE: All display features returned with this container must be cropped to the application
      * window and reported within the coordinate space of the window that was provided by the app.
-     * @see ExtensionInterface#getWindowLayoutInfo(Context)
      */
     @NonNull
     private List<ExtensionDisplayFeature> mDisplayFeatures;
diff --git a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
index 77bc4de..5710f79 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
@@ -22,8 +22,7 @@
 import android.widget.FrameLayout
 import android.widget.TextView
 import androidx.core.util.Consumer
-import androidx.window.DeviceState
-import androidx.window.DisplayFeature
+import androidx.window.FoldingFeature
 import androidx.window.WindowLayoutInfo
 import androidx.window.WindowManager
 import java.text.SimpleDateFormat
@@ -53,20 +52,12 @@
 
     override fun onStart() {
         super.onStart()
-        windowManager.registerDeviceStateChangeCallback(
-            mainThreadExecutor,
-            stateContainer.stateConsumer
-        )
-        windowManager.registerLayoutChangeCallback(
-            mainThreadExecutor,
-            stateContainer.layoutConsumer
-        )
+        windowManager.registerLayoutChangeCallback(mainThreadExecutor, stateContainer)
     }
 
     override fun onStop() {
         super.onStop()
-        windowManager.unregisterDeviceStateChangeCallback(stateContainer.stateConsumer)
-        windowManager.unregisterLayoutChangeCallback(stateContainer.layoutConsumer)
+        windowManager.unregisterLayoutChangeCallback(stateContainer)
     }
 
     /** Updates the device state and display feature positions. */
@@ -80,13 +71,6 @@
 
         // Update the UI with the current state
         val stateStringBuilder = StringBuilder()
-        // Update the current state string
-        stateContainer.lastState?.let { deviceState ->
-            stateStringBuilder.append(getString(R.string.deviceState))
-                .append(": ")
-                .append(deviceState)
-                .append("\n")
-        }
 
         stateContainer.lastLayoutInfo?.let { windowLayoutInfo ->
             stateStringBuilder.append(getString(R.string.windowLayout))
@@ -107,9 +91,10 @@
                 }
 
                 val featureView = View(this)
-                val color = when (displayFeature.type) {
-                    DisplayFeature.TYPE_FOLD -> getColor(R.color.colorFeatureFold)
-                    DisplayFeature.TYPE_HINGE -> getColor(R.color.colorFeatureHinge)
+                val foldFeature = displayFeature as? FoldingFeature
+                val color = when (foldFeature?.type) {
+                    FoldingFeature.TYPE_FOLD -> getColor(R.color.colorFeatureFold)
+                    FoldingFeature.TYPE_HINGE -> getColor(R.color.colorFeatureHinge)
                     else -> getColor(R.color.colorFeatureUnknown)
                 }
                 featureView.foreground = ColorDrawable(color)
@@ -139,25 +124,10 @@
         return currentDate.toString()
     }
 
-    inner class StateContainer {
-        var lastState: DeviceState? = null
+    inner class StateContainer : Consumer<WindowLayoutInfo> {
         var lastLayoutInfo: WindowLayoutInfo? = null
 
-        val stateConsumer: Consumer<DeviceState>
-        val layoutConsumer: Consumer<WindowLayoutInfo>
-
-        init {
-            stateConsumer = Consumer { state: DeviceState -> update(state) }
-            layoutConsumer = Consumer { layout: WindowLayoutInfo -> update(layout) }
-        }
-
-        fun update(newDeviceState: DeviceState) {
-            updateStateLog(newDeviceState)
-            lastState = newDeviceState
-            updateCurrentState()
-        }
-
-        fun update(newLayoutInfo: WindowLayoutInfo) {
+        override fun accept(newLayoutInfo: WindowLayoutInfo) {
             updateStateLog(newLayoutInfo)
             lastLayoutInfo = newLayoutInfo
             updateCurrentState()
diff --git a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
index 5e43a732..2cc38db 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
@@ -28,7 +28,8 @@
 import android.widget.TextView
 import android.widget.Toast
 import androidx.core.util.Consumer
-import androidx.window.DeviceState
+import androidx.window.FoldingFeature
+import androidx.window.WindowLayoutInfo
 import androidx.window.WindowManager
 
 /**
@@ -39,7 +40,7 @@
     private val TAG = "FoldablePresentation"
 
     private lateinit var windowManager: WindowManager
-    private val deviceStateChangeCallback = DeviceStateChangeCallback()
+    private val deviceStateChangeCallback = WindowLayoutInfoChangeCallback()
     private var presentation: DemoPresentation? = null
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -48,7 +49,7 @@
 
         windowManager = getTestBackend()?.let { backend -> WindowManager(this, backend) }
             ?: WindowManager(this)
-        windowManager.registerDeviceStateChangeCallback(
+        windowManager.registerLayoutChangeCallback(
             mainThreadExecutor,
             deviceStateChangeCallback
         )
@@ -56,7 +57,7 @@
 
     override fun onDestroy() {
         super.onDestroy()
-        windowManager.unregisterDeviceStateChangeCallback(deviceStateChangeCallback)
+        windowManager.unregisterLayoutChangeCallback(deviceStateChangeCallback)
     }
 
     internal fun startPresentation(context: Context) {
@@ -137,26 +138,36 @@
     }
 
     /**
-     * Updates the display of the current device state.
+     * Updates the display of the current fold feature state.
      */
-    internal fun updateCurrentState(deviceState: DeviceState) {
+    internal fun updateCurrentState(info: WindowLayoutInfo) {
         val stateStringBuilder = StringBuilder()
+
         stateStringBuilder.append(getString(R.string.deviceState))
             .append(": ")
-            .append(deviceState)
-            .append("\n")
+
+        info.displayFeatures
+            .mapNotNull { it as? FoldingFeature }
+            .forEach { feature ->
+                stateStringBuilder.append(feature.stateString())
+                    .append("\n")
+            }
 
         findViewById<TextView>(R.id.currentState).text = stateStringBuilder.toString()
     }
 
-    inner class DeviceStateChangeCallback : Consumer<DeviceState> {
-        override fun accept(newDeviceState: DeviceState) {
-            updateCurrentState(newDeviceState)
-            if (newDeviceState.posture == DeviceState.POSTURE_CLOSED) {
-                startPresentation(this@PresentationActivity)
-            } else {
-                stopPresentation(null)
-            }
+    private fun FoldingFeature.stateString(): String {
+        return when (state) {
+            FoldingFeature.STATE_FLAT -> "FLAT"
+            FoldingFeature.STATE_FLIPPED -> "FLIPPED"
+            FoldingFeature.STATE_HALF_OPENED -> "HALF_OPENED"
+            else -> "Unknown feature state ($state)"
+        }
+    }
+
+    inner class WindowLayoutInfoChangeCallback : Consumer<WindowLayoutInfo> {
+        override fun accept(info: WindowLayoutInfo) {
+            updateCurrentState(info)
         }
     }
 }
diff --git a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt b/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
index 954c798..cc0b341 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
@@ -23,8 +23,10 @@
 import android.view.View.MeasureSpec.AT_MOST
 import android.view.View.MeasureSpec.EXACTLY
 import android.widget.FrameLayout
-import androidx.window.DisplayFeature.TYPE_FOLD
-import androidx.window.DisplayFeature.TYPE_HINGE
+import androidx.window.DisplayFeature
+import androidx.window.FoldingFeature
+import androidx.window.FoldingFeature.TYPE_FOLD
+import androidx.window.FoldingFeature.TYPE_HINGE
 import androidx.window.WindowLayoutInfo
 
 /**
@@ -41,9 +43,11 @@
     private var lastHeightMeasureSpec: Int = 0
 
     constructor(context: Context) : super(context)
+
     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
         setAttributes(attrs)
     }
+
     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
         context,
         attrs,
@@ -125,46 +129,43 @@
         val paddedWidth = width - paddingLeft - paddingRight
         val paddedHeight = height - paddingTop - paddingBottom
 
-        for (feature in windowLayoutInfo?.displayFeatures!!) {
-            // Only a hinge or a fold can split the area in two
-            if (feature.type != TYPE_FOLD && feature.type != TYPE_HINGE) {
-                continue
-            }
+        windowLayoutInfo?.displayFeatures
+            ?.firstOrNull { feature -> isValidFoldFeature(feature) }
+            ?.let { feature ->
+                getFeaturePositionInViewRect(feature, this)?.let {
+                    if (feature.bounds.left == 0) { // Horizontal layout
+                        val topRect = Rect(
+                            paddingLeft, paddingTop,
+                            paddingLeft + paddedWidth, it.top
+                        )
+                        val bottomRect = Rect(
+                            paddingLeft, it.bottom,
+                            paddingLeft + paddedWidth, paddingTop + paddedHeight
+                        )
 
-            val splitRect = getFeaturePositionInViewRect(feature, this) ?: continue
+                        if (measureAndCheckMinSize(topRect, startView) &&
+                            measureAndCheckMinSize(bottomRect, endView)
+                        ) {
+                            return arrayOf(topRect, bottomRect)
+                        }
+                    } else if (feature.bounds.top == 0) { // Vertical layout
+                        val leftRect = Rect(
+                            paddingLeft, paddingTop,
+                            it.left, paddingTop + paddedHeight
+                        )
+                        val rightRect = Rect(
+                            it.right, paddingTop,
+                            paddingLeft + paddedWidth, paddingTop + paddedHeight
+                        )
 
-            if (feature.bounds.left == 0) { // Horizontal layout
-                val topRect = Rect(
-                    paddingLeft, paddingTop,
-                    paddingLeft + paddedWidth, splitRect.top
-                )
-                val bottomRect = Rect(
-                    paddingLeft, splitRect.bottom,
-                    paddingLeft + paddedWidth, paddingTop + paddedHeight
-                )
-
-                if (measureAndCheckMinSize(topRect, startView) &&
-                    measureAndCheckMinSize(bottomRect, endView)
-                ) {
-                    return arrayOf(topRect, bottomRect)
-                }
-            } else if (feature.bounds.top == 0) { // Vertical layout
-                val leftRect = Rect(
-                    paddingLeft, paddingTop,
-                    splitRect.left, paddingTop + paddedHeight
-                )
-                val rightRect = Rect(
-                    splitRect.right, paddingTop,
-                    paddingLeft + paddedWidth, paddingTop + paddedHeight
-                )
-
-                if (measureAndCheckMinSize(leftRect, startView) &&
-                    measureAndCheckMinSize(rightRect, endView)
-                ) {
-                    return arrayOf(leftRect, rightRect)
+                        if (measureAndCheckMinSize(leftRect, startView) &&
+                            measureAndCheckMinSize(rightRect, endView)
+                        ) {
+                            return arrayOf(leftRect, rightRect)
+                        }
+                    }
                 }
             }
-        }
 
         // We have tried to fit the children and measured them previously. Since they didn't fit,
         // we need to measure again to update the stored values.
@@ -191,4 +192,10 @@
         return childView.measuredWidthAndState and MEASURED_STATE_TOO_SMALL == 0 &&
             childView.measuredHeightAndState and MEASURED_STATE_TOO_SMALL == 0
     }
+
+    private fun isValidFoldFeature(displayFeature: DisplayFeature): Boolean {
+        val feature = displayFeature as? FoldingFeature ?: return false
+        return (feature.type == TYPE_FOLD || feature.type == TYPE_HINGE) &&
+            getFeaturePositionInViewRect(feature, this) != null
+    }
 }
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
index 5ee6df2..27754f4 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("DEPRECATION") // TODO(b/173739071) Remove DeviceState
+
 package androidx.window.sample.backend
 
 import android.app.Activity
@@ -22,7 +24,7 @@
 import androidx.core.util.Consumer
 import androidx.window.DeviceState
 import androidx.window.DisplayFeature
-import androidx.window.DisplayFeature.TYPE_FOLD
+import androidx.window.FoldingFeature
 import androidx.window.WindowBackend
 import androidx.window.WindowLayoutInfo
 import java.util.concurrent.Executor
@@ -53,18 +55,16 @@
         SHORT_DIMENSION
     }
 
-    private fun getDeviceState(): DeviceState {
-        return DeviceState.Builder().setPosture(DeviceState.POSTURE_OPENED).build()
-    }
-
     private fun getWindowLayoutInfo(activity: Activity): WindowLayoutInfo {
         val windowSize = activity.calculateWindowSizeExt()
         val featureRect = foldRect(windowSize)
 
-        val displayFeature = DisplayFeature.Builder()
-            .setBounds(featureRect)
-            .setType(TYPE_FOLD)
-            .build()
+        val displayFeature =
+            FoldingFeature(
+                featureRect,
+                FoldingFeature.TYPE_FOLD,
+                FoldingFeature.STATE_FLAT
+            )
         val featureList = ArrayList<DisplayFeature>()
         featureList.add(displayFeature)
         return WindowLayoutInfo.Builder().setDisplayFeatures(featureList).build()
@@ -96,9 +96,7 @@
     override fun registerDeviceStateChangeCallback(
         executor: Executor,
         callback: Consumer<DeviceState>
-    ) {
-        executor.execute { callback.accept(getDeviceState()) }
-    }
+    ) {}
 
     override fun unregisterDeviceStateChangeCallback(callback: Consumer<DeviceState>) {
     }
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 3abcf8b..3055076 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -1,35 +1,37 @@
 // Signature format: 4.0
 package androidx.window {
 
-  public final class DeviceState {
-    method public int getPosture();
-    field public static final int POSTURE_CLOSED = 1; // 0x1
-    field public static final int POSTURE_FLIPPED = 4; // 0x4
-    field public static final int POSTURE_HALF_OPENED = 2; // 0x2
-    field public static final int POSTURE_OPENED = 3; // 0x3
-    field public static final int POSTURE_UNKNOWN = 0; // 0x0
+  @Deprecated public final class DeviceState {
+    method @Deprecated public int getPosture();
+    field @Deprecated public static final int POSTURE_CLOSED = 1; // 0x1
+    field @Deprecated public static final int POSTURE_FLIPPED = 4; // 0x4
+    field @Deprecated public static final int POSTURE_HALF_OPENED = 2; // 0x2
+    field @Deprecated public static final int POSTURE_OPENED = 3; // 0x3
+    field @Deprecated public static final int POSTURE_UNKNOWN = 0; // 0x0
   }
 
-  public static final class DeviceState.Builder {
-    ctor public DeviceState.Builder();
-    method public androidx.window.DeviceState build();
-    method public androidx.window.DeviceState.Builder setPosture(int);
+  @Deprecated public static final class DeviceState.Builder {
+    ctor @Deprecated public DeviceState.Builder();
+    method @Deprecated public androidx.window.DeviceState build();
+    method @Deprecated public androidx.window.DeviceState.Builder setPosture(int);
   }
 
-  public final class DisplayFeature {
+  public interface DisplayFeature {
     method public android.graphics.Rect getBounds();
+  }
+
+  public class FoldingFeature implements androidx.window.DisplayFeature {
+    ctor public FoldingFeature(android.graphics.Rect, int, int);
+    method public android.graphics.Rect getBounds();
+    method public int getState();
     method public int getType();
+    field public static final int STATE_FLAT = 1; // 0x1
+    field public static final int STATE_FLIPPED = 3; // 0x3
+    field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
-  public static final class DisplayFeature.Builder {
-    ctor public DisplayFeature.Builder();
-    method public androidx.window.DisplayFeature build();
-    method public androidx.window.DisplayFeature.Builder setBounds(android.graphics.Rect);
-    method public androidx.window.DisplayFeature.Builder setType(int);
-  }
-
   public interface WindowBackend {
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -52,9 +54,9 @@
     ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
     method public androidx.window.WindowMetrics getCurrentWindowMetrics();
     method public androidx.window.WindowMetrics getMaximumWindowMetrics();
-    method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
+    method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
+    method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 3abcf8b..3055076 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -1,35 +1,37 @@
 // Signature format: 4.0
 package androidx.window {
 
-  public final class DeviceState {
-    method public int getPosture();
-    field public static final int POSTURE_CLOSED = 1; // 0x1
-    field public static final int POSTURE_FLIPPED = 4; // 0x4
-    field public static final int POSTURE_HALF_OPENED = 2; // 0x2
-    field public static final int POSTURE_OPENED = 3; // 0x3
-    field public static final int POSTURE_UNKNOWN = 0; // 0x0
+  @Deprecated public final class DeviceState {
+    method @Deprecated public int getPosture();
+    field @Deprecated public static final int POSTURE_CLOSED = 1; // 0x1
+    field @Deprecated public static final int POSTURE_FLIPPED = 4; // 0x4
+    field @Deprecated public static final int POSTURE_HALF_OPENED = 2; // 0x2
+    field @Deprecated public static final int POSTURE_OPENED = 3; // 0x3
+    field @Deprecated public static final int POSTURE_UNKNOWN = 0; // 0x0
   }
 
-  public static final class DeviceState.Builder {
-    ctor public DeviceState.Builder();
-    method public androidx.window.DeviceState build();
-    method public androidx.window.DeviceState.Builder setPosture(int);
+  @Deprecated public static final class DeviceState.Builder {
+    ctor @Deprecated public DeviceState.Builder();
+    method @Deprecated public androidx.window.DeviceState build();
+    method @Deprecated public androidx.window.DeviceState.Builder setPosture(int);
   }
 
-  public final class DisplayFeature {
+  public interface DisplayFeature {
     method public android.graphics.Rect getBounds();
+  }
+
+  public class FoldingFeature implements androidx.window.DisplayFeature {
+    ctor public FoldingFeature(android.graphics.Rect, int, int);
+    method public android.graphics.Rect getBounds();
+    method public int getState();
     method public int getType();
+    field public static final int STATE_FLAT = 1; // 0x1
+    field public static final int STATE_FLIPPED = 3; // 0x3
+    field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
-  public static final class DisplayFeature.Builder {
-    ctor public DisplayFeature.Builder();
-    method public androidx.window.DisplayFeature build();
-    method public androidx.window.DisplayFeature.Builder setBounds(android.graphics.Rect);
-    method public androidx.window.DisplayFeature.Builder setType(int);
-  }
-
   public interface WindowBackend {
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -52,9 +54,9 @@
     ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
     method public androidx.window.WindowMetrics getCurrentWindowMetrics();
     method public androidx.window.WindowMetrics getMaximumWindowMetrics();
-    method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
+    method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
+    method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 3abcf8b..3055076 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -1,35 +1,37 @@
 // Signature format: 4.0
 package androidx.window {
 
-  public final class DeviceState {
-    method public int getPosture();
-    field public static final int POSTURE_CLOSED = 1; // 0x1
-    field public static final int POSTURE_FLIPPED = 4; // 0x4
-    field public static final int POSTURE_HALF_OPENED = 2; // 0x2
-    field public static final int POSTURE_OPENED = 3; // 0x3
-    field public static final int POSTURE_UNKNOWN = 0; // 0x0
+  @Deprecated public final class DeviceState {
+    method @Deprecated public int getPosture();
+    field @Deprecated public static final int POSTURE_CLOSED = 1; // 0x1
+    field @Deprecated public static final int POSTURE_FLIPPED = 4; // 0x4
+    field @Deprecated public static final int POSTURE_HALF_OPENED = 2; // 0x2
+    field @Deprecated public static final int POSTURE_OPENED = 3; // 0x3
+    field @Deprecated public static final int POSTURE_UNKNOWN = 0; // 0x0
   }
 
-  public static final class DeviceState.Builder {
-    ctor public DeviceState.Builder();
-    method public androidx.window.DeviceState build();
-    method public androidx.window.DeviceState.Builder setPosture(int);
+  @Deprecated public static final class DeviceState.Builder {
+    ctor @Deprecated public DeviceState.Builder();
+    method @Deprecated public androidx.window.DeviceState build();
+    method @Deprecated public androidx.window.DeviceState.Builder setPosture(int);
   }
 
-  public final class DisplayFeature {
+  public interface DisplayFeature {
     method public android.graphics.Rect getBounds();
+  }
+
+  public class FoldingFeature implements androidx.window.DisplayFeature {
+    ctor public FoldingFeature(android.graphics.Rect, int, int);
+    method public android.graphics.Rect getBounds();
+    method public int getState();
     method public int getType();
+    field public static final int STATE_FLAT = 1; // 0x1
+    field public static final int STATE_FLIPPED = 3; // 0x3
+    field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
-  public static final class DisplayFeature.Builder {
-    ctor public DisplayFeature.Builder();
-    method public androidx.window.DisplayFeature build();
-    method public androidx.window.DisplayFeature.Builder setBounds(android.graphics.Rect);
-    method public androidx.window.DisplayFeature.Builder setType(int);
-  }
-
   public interface WindowBackend {
     method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -52,9 +54,9 @@
     ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
     method public androidx.window.WindowMetrics getCurrentWindowMetrics();
     method public androidx.window.WindowMetrics getMaximumWindowMetrics();
-    method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
+    method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
+    method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 30a2fa6..4b98a67 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -18,12 +18,16 @@
 import androidx.build.LibraryVersions
 import androidx.build.Publish
 
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_CORE
 import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_EXT_JUNIT
 import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RULES
 import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RUNNER
 import static androidx.build.dependencies.DependenciesKt.DEXMAKER_MOCKITO
+import static androidx.build.dependencies.DependenciesKt.JUNIT
 import static androidx.build.dependencies.DependenciesKt.MOCKITO_CORE
+import static androidx.build.dependencies.DependenciesKt.ROBOLECTRIC
 import static androidx.build.dependencies.DependenciesKt.TRUTH
+import static androidx.build.dependencies.DependenciesKt.getKOTLIN_STDLIB
 
 plugins {
     id("AndroidXPlugin")
@@ -47,6 +51,16 @@
     compileOnly(project(":window:window-extensions"))
     compileOnly(project(":window:window-sidecar"))
 
+    testImplementation(KOTLIN_STDLIB)
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RUNNER)
+    testImplementation(JUNIT)
+    testImplementation(TRUTH)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+    testImplementation(compileOnly(project(":window:window-extensions")))
+    testImplementation(compileOnly(project(":window:window-sidecar")))
+
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ANDROIDX_TEST_RULES)
diff --git a/window/window/src/androidTest/java/androidx/window/DisplayFeatureTest.java b/window/window/src/androidTest/java/androidx/window/DisplayFeatureTest.java
deleted file mode 100644
index 93033ae..0000000
--- a/window/window/src/androidTest/java/androidx/window/DisplayFeatureTest.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-
-import android.graphics.Rect;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link DisplayFeature} class. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public final class DisplayFeatureTest {
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testBuilder_empty() {
-        new DisplayFeature.Builder().build();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testBuilder_foldWithNonZeroArea() {
-        DisplayFeature feature = new DisplayFeature.Builder()
-                .setBounds(new Rect(10, 0, 20, 30))
-                .setType(DisplayFeature.TYPE_FOLD).build();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testBuilder_horizontalHingeWithNonZeroOrigin() {
-        DisplayFeature horizontalHinge = new DisplayFeature.Builder()
-                .setBounds(new Rect(1, 10, 20, 10))
-                .setType(DisplayFeature.TYPE_HINGE).build();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testBuilder_verticalHingeWithNonZeroOrigin() {
-        DisplayFeature verticalHinge = new DisplayFeature.Builder()
-                .setBounds(new Rect(10, 1, 10, 20))
-                .setType(DisplayFeature.TYPE_HINGE).build();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testBuilder_horizontalFoldWithNonZeroOrigin() {
-        DisplayFeature horizontalFold = new DisplayFeature.Builder()
-                .setBounds(new Rect(1, 10, 20, 10))
-                .setType(DisplayFeature.TYPE_FOLD).build();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testBuilder_verticalFoldWithNonZeroOrigin() {
-        DisplayFeature verticalFold = new DisplayFeature.Builder()
-                .setBounds(new Rect(10, 1, 10, 20))
-                .setType(DisplayFeature.TYPE_FOLD).build();
-    }
-
-    @Test
-    public void testBuilder_setBoundsAndType() {
-        DisplayFeature.Builder builder = new DisplayFeature.Builder();
-        Rect bounds = new Rect(0, 10, 30, 10);
-        builder.setBounds(bounds);
-        builder.setType(DisplayFeature.TYPE_HINGE);
-        DisplayFeature feature = builder.build();
-
-        assertEquals(bounds, feature.getBounds());
-        assertEquals(DisplayFeature.TYPE_HINGE, feature.getType());
-    }
-
-    @Test
-    public void testEquals_sameAttributes() {
-        Rect bounds = new Rect(1, 0, 1, 10);
-        int type = DisplayFeature.TYPE_FOLD;
-
-        DisplayFeature original = new DisplayFeature(bounds, type);
-        DisplayFeature copy = new DisplayFeature(bounds, type);
-
-        assertEquals(original, copy);
-    }
-
-    @Test
-    public void testEquals_differentRect() {
-        Rect originalRect = new Rect(1, 0, 1, 10);
-        Rect otherRect = new Rect(2, 0, 2, 10);
-        int type = DisplayFeature.TYPE_FOLD;
-
-        DisplayFeature original = new DisplayFeature(originalRect, type);
-        DisplayFeature other = new DisplayFeature(otherRect, type);
-
-        assertNotEquals(original, other);
-    }
-
-    @Test
-    public void testEquals_differentType() {
-        Rect rect = new Rect(1, 0, 1, 10);
-        int originalType = DisplayFeature.TYPE_FOLD;
-        int otherType = DisplayFeature.TYPE_HINGE;
-
-        DisplayFeature original = new DisplayFeature(rect, originalType);
-        DisplayFeature other = new DisplayFeature(rect, otherType);
-
-        assertNotEquals(original, other);
-    }
-
-    @Test
-    public void testHashCode_matchesIfEqual() {
-        Rect originalRect = new Rect(1, 0, 1, 10);
-        Rect matchingRect = new Rect(1, 0, 1, 10);
-        int type = DisplayFeature.TYPE_FOLD;
-
-        DisplayFeature original = new DisplayFeature(originalRect, type);
-        DisplayFeature matching = new DisplayFeature(matchingRect, type);
-
-        assertEquals(original, matching);
-        assertEquals(original.hashCode(), matching.hashCode());
-    }
-}
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java
index c6e6348..9932500 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java
@@ -25,6 +25,7 @@
 
 import androidx.window.extensions.ExtensionDeviceState;
 import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
 import androidx.window.extensions.ExtensionWindowLayoutInfo;
 
 import org.junit.After;
@@ -64,8 +65,8 @@
     public void testTranslate_validFeature() {
         Activity mockActivity = mock(Activity.class);
         Rect bounds = new Rect(WINDOW_BOUNDS.left, 0, WINDOW_BOUNDS.right, 0);
-        ExtensionDisplayFeature foldFeature = new ExtensionDisplayFeature(bounds,
-                ExtensionDisplayFeature.TYPE_FOLD);
+        ExtensionDisplayFeature foldFeature = new ExtensionFoldingFeature(bounds,
+                ExtensionFoldingFeature.TYPE_FOLD, ExtensionFoldingFeature.STATE_FLAT);
 
         List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
         extensionDisplayFeatures.add(foldFeature);
@@ -73,7 +74,8 @@
                 new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
 
         List<DisplayFeature> expectedFeatures = new ArrayList<>();
-        expectedFeatures.add(new DisplayFeature(foldFeature.getBounds(), DisplayFeature.TYPE_FOLD));
+        expectedFeatures.add(new FoldingFeature(foldFeature.getBounds(), FoldingFeature.TYPE_FOLD,
+                FoldingFeature.STATE_FLAT));
         WindowLayoutInfo expected = new WindowLayoutInfo(expectedFeatures);
 
         ExtensionAdapter adapter = new ExtensionAdapter();
@@ -85,59 +87,17 @@
 
     @Test
     @Override
-    public void testTranslateWindowLayoutInfo_filterRemovesEmptyBoundsFeature() {
-        List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
-        extensionDisplayFeatures.add(
-                new ExtensionDisplayFeature(new Rect(), ExtensionDisplayFeature.TYPE_FOLD));
-
-        ExtensionAdapter adapter = new ExtensionAdapter();
-        ExtensionWindowLayoutInfo windowLayoutInfo =
-                new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
-        Activity mockActivity = mock(Activity.class);
-
-        WindowLayoutInfo actual = adapter.translate(mockActivity, windowLayoutInfo);
-
-        assertTrue("Remove empty bounds feature", actual.getDisplayFeatures().isEmpty());
-    }
-
-
-    @Test
-    @Override
-    public void testTranslateWindowLayoutInfo_filterRemovesNonEmptyAreaFoldFeature() {
-        List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
-        Rect fullWidthBounds = new Rect(0, 1, WINDOW_BOUNDS.width(), 2);
-        Rect fullHeightBounds = new Rect(1, 0, 2, WINDOW_BOUNDS.height());
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
-                ExtensionDisplayFeature.TYPE_FOLD));
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
-                ExtensionDisplayFeature.TYPE_FOLD));
-
-        ExtensionAdapter extensionCallbackAdapter = new ExtensionAdapter();
-        ExtensionWindowLayoutInfo windowLayoutInfo =
-                new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
-        Activity mockActivity = mock(Activity.class);
-
-        WindowLayoutInfo actual = extensionCallbackAdapter.translate(mockActivity,
-                windowLayoutInfo);
-
-        assertTrue("Remove non empty area fold feature", actual.getDisplayFeatures().isEmpty());
-    }
-
-    @Test
-    @Override
     public void testTranslateWindowLayoutInfo_filterRemovesHingeFeatureNotSpanningFullDimension() {
         List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
         Rect fullWidthBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
                 WINDOW_BOUNDS.right / 2, 2);
         Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, 2,
                 WINDOW_BOUNDS.bottom / 2);
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
 
-        ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
-                ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
         ExtensionAdapter extensionCallbackAdapter = new ExtensionAdapter();
         ExtensionWindowLayoutInfo windowLayoutInfo =
                 new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
@@ -159,10 +119,10 @@
                 WINDOW_BOUNDS.right / 2, WINDOW_BOUNDS.top);
         Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.left,
                 WINDOW_BOUNDS.bottom / 2);
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
 
         ExtensionAdapter adapter = new ExtensionAdapter();
         ExtensionWindowLayoutInfo windowLayoutInfo =
@@ -183,20 +143,14 @@
         List<DeviceState> values = new ArrayList<>();
 
         values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
-                ExtensionDeviceState.POSTURE_UNKNOWN)));
-        values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
-                ExtensionDeviceState.POSTURE_CLOSED)));
-        values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
                 ExtensionDeviceState.POSTURE_HALF_OPENED)));
         values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
                 ExtensionDeviceState.POSTURE_OPENED)));
         values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
                 ExtensionDeviceState.POSTURE_FLIPPED)));
 
-        assertEquals(DeviceState.POSTURE_UNKNOWN, values.get(0).getPosture());
-        assertEquals(DeviceState.POSTURE_CLOSED, values.get(1).getPosture());
-        assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(2).getPosture());
-        assertEquals(DeviceState.POSTURE_OPENED, values.get(3).getPosture());
-        assertEquals(DeviceState.POSTURE_FLIPPED, values.get(4).getPosture());
+        assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(0).getPosture());
+        assertEquals(DeviceState.POSTURE_OPENED, values.get(1).getPosture());
+        assertEquals(DeviceState.POSTURE_FLIPPED, values.get(2).getPosture());
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
index f2d1f92..c0d318e 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
@@ -17,11 +17,12 @@
 package androidx.window;
 
 import static androidx.window.ExtensionInterfaceCompat.ExtensionCallbackInterface;
-import static androidx.window.TestBoundUtil.invalidFoldBounds;
-import static androidx.window.TestBoundUtil.invalidHingeBounds;
-import static androidx.window.TestBoundUtil.validFoldBound;
+import static androidx.window.TestBoundsUtil.invalidFoldBounds;
+import static androidx.window.TestBoundsUtil.invalidHingeBounds;
+import static androidx.window.TestBoundsUtil.validFoldBound;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -37,6 +38,7 @@
 import androidx.test.filters.LargeTest;
 import androidx.window.extensions.ExtensionDeviceState;
 import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
 import androidx.window.extensions.ExtensionInterface;
 import androidx.window.extensions.ExtensionWindowLayoutInfo;
 
@@ -62,19 +64,17 @@
     private static final Rect WINDOW_BOUNDS = new Rect(0, 0, 50, 100);
 
     ExtensionCompat mExtensionCompat;
-    private ExtensionInterface mMockExtensionInterface;
     private Activity mActivity;
 
     @Before
     public void setUp() {
-        mMockExtensionInterface = mock(ExtensionInterface.class);
-        mExtensionCompat = new ExtensionCompat(mMockExtensionInterface, new ExtensionAdapter());
+        mExtensionCompat = new ExtensionCompat(mock(ExtensionInterface.class),
+                new ExtensionAdapter());
         mActivity = mock(Activity.class);
 
         TestWindowBoundsHelper mWindowBoundsHelper = new TestWindowBoundsHelper();
         mWindowBoundsHelper.setCurrentBounds(WINDOW_BOUNDS);
         WindowBoundsHelper.setForTesting(mWindowBoundsHelper);
-
     }
 
     @After
@@ -141,7 +141,8 @@
         mExtensionCompat.onWindowLayoutChangeListenerAdded(mActivity);
         Rect bounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.width(), 1);
         ExtensionDisplayFeature extensionDisplayFeature =
-                new ExtensionDisplayFeature(bounds, ExtensionDisplayFeature.TYPE_HINGE);
+                new ExtensionFoldingFeature(bounds, ExtensionFoldingFeature.TYPE_HINGE,
+                        ExtensionFoldingFeature.STATE_FLIPPED);
         List<ExtensionDisplayFeature> displayFeatures = new ArrayList<>();
         displayFeatures.add(extensionDisplayFeature);
         ExtensionWindowLayoutInfo extensionWindowLayoutInfo =
@@ -156,7 +157,10 @@
         WindowLayoutInfo capturedLayout = windowLayoutInfoCaptor.getValue();
         assertEquals(1, capturedLayout.getDisplayFeatures().size());
         DisplayFeature capturedDisplayFeature = capturedLayout.getDisplayFeatures().get(0);
-        assertEquals(DisplayFeature.TYPE_HINGE, capturedDisplayFeature.getType());
+
+        FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
+        assertNotNull(foldingFeature);
+        assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
         assertEquals(bounds, capturedDisplayFeature.getBounds());
     }
 
@@ -263,13 +267,15 @@
             List<ExtensionDisplayFeature> malformedFeatures = new ArrayList<>();
 
             for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
-                malformedFeatures.add(new ExtensionDisplayFeature(malformedBound,
-                        ExtensionDisplayFeature.TYPE_FOLD));
+                malformedFeatures.add(new ExtensionFoldingFeature(malformedBound,
+                        ExtensionFoldingFeature.TYPE_FOLD,
+                        ExtensionFoldingFeature.STATE_FLAT));
             }
 
             for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
-                malformedFeatures.add(new ExtensionDisplayFeature(malformedBound,
-                        ExtensionDisplayFeature.TYPE_HINGE));
+                malformedFeatures.add(new ExtensionFoldingFeature(malformedBound,
+                        ExtensionFoldingFeature.TYPE_HINGE,
+                        ExtensionFoldingFeature.STATE_FLAT));
             }
 
             return new ExtensionWindowLayoutInfo(malformedFeatures);
@@ -278,8 +284,8 @@
         private ExtensionWindowLayoutInfo validWindowLayoutInfo() {
             List<ExtensionDisplayFeature> validFeatures = new ArrayList<>();
 
-            validFeatures.add(new ExtensionDisplayFeature(validFoldBound(WINDOW_BOUNDS),
-                    ExtensionDisplayFeature.TYPE_FOLD));
+            validFeatures.add(new ExtensionFoldingFeature(validFoldBound(WINDOW_BOUNDS),
+                    ExtensionFoldingFeature.TYPE_FOLD, ExtensionFoldingFeature.STATE_FLAT));
 
             return new ExtensionWindowLayoutInfo(validFeatures);
         }
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
index 3e89816..a34be4a 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
@@ -20,8 +20,6 @@
 import static androidx.window.ExtensionWindowBackend.initAndVerifyExtension;
 import static androidx.window.Version.VERSION_0_1;
 import static androidx.window.Version.VERSION_1_0;
-import static androidx.window.extensions.ExtensionDisplayFeature.TYPE_FOLD;
-import static androidx.window.extensions.ExtensionDisplayFeature.TYPE_HINGE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -35,6 +33,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.graphics.Rect;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -42,7 +41,7 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.rule.ActivityTestRule;
 import androidx.window.extensions.ExtensionDeviceState;
-import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -79,8 +78,6 @@
     public void testDeviceStateCallback() {
         assumeExtensionV10_V01();
         final Set<Integer> validValues = new HashSet<>();
-        validValues.add(ExtensionDeviceState.POSTURE_UNKNOWN);
-        validValues.add(ExtensionDeviceState.POSTURE_CLOSED);
         validValues.add(ExtensionDeviceState.POSTURE_FLIPPED);
         validValues.add(ExtensionDeviceState.POSTURE_HALF_OPENED);
         validValues.add(ExtensionDeviceState.POSTURE_OPENED);
@@ -89,8 +86,7 @@
         extension.setExtensionCallback(callbackInterface);
         extension.onDeviceStateListenersChanged(false);
 
-        verify(callbackInterface).onDeviceStateChanged(argThat(
-                deviceState -> validValues.contains(deviceState.getPosture())));
+        verify(callbackInterface, atLeastOnce()).onDeviceStateChanged(any());
     }
 
     @Test
@@ -106,15 +102,16 @@
     public void testDisplayFeatureDataClass() {
         assumeExtensionV10_V01();
 
-        Rect rect = new Rect(1, 2, 3, 4);
+        Rect rect = new Rect(0, 100, 100, 100);
         int type = 1;
-        ExtensionDisplayFeature displayFeature = new ExtensionDisplayFeature(rect, type);
+        int state = 1;
+        ExtensionFoldingFeature displayFeature =
+                new ExtensionFoldingFeature(rect, type, state);
         assertEquals(rect, displayFeature.getBounds());
-        assertEquals(type, displayFeature.getType());
     }
 
     @Test
-    public void testWindowLayoutInfoCallback() {
+    public void testWindowLayoutCallback() {
         assumeExtensionV10_V01();
         ExtensionInterfaceCompat extension = initAndVerifyExtension(mContext);
         ExtensionCallbackInterface callbackInterface = mock(ExtensionCallbackInterface.class);
@@ -125,7 +122,7 @@
 
         assertTrue("Layout must happen after launch", activity.waitForLayout());
 
-        verify(callbackInterface).onWindowLayoutChanged(any(), argThat(
+        verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(any(), argThat(
                 new WindowLayoutInfoValidator(activity)));
     }
 
@@ -155,14 +152,30 @@
         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
 
         activity.waitForLayout();
+        if (activity.getResources().getConfiguration().orientation
+                != Configuration.ORIENTATION_PORTRAIT) {
+            // Orientation change did not occur on this device config. Skipping the test.
+            return;
+        }
 
         activity.resetLayoutCounter();
         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
 
-        assertTrue("Layout must happen after orientation change", activity.waitForLayout());
+        boolean layoutHappened = activity.waitForLayout();
+        if (activity.getResources().getConfiguration().orientation
+                != Configuration.ORIENTATION_LANDSCAPE) {
+            // Orientation change did not occur on this device config. Skipping the test.
+            return;
+        }
+        assertTrue("Layout must happen after orientation change", layoutHappened);
 
-        verify(callbackInterface, atLeastOnce())
-                .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+        if (!isSidecar()) {
+            verify(callbackInterface, atLeastOnce())
+                    .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+        } else {
+            verify(callbackInterface, atLeastOnce())
+                    .onWindowLayoutChanged(any(), any());
+        }
     }
 
     @Test
@@ -181,6 +194,11 @@
         activity = mActivityTestRule.getActivity();
 
         activity.waitForLayout();
+        if (activity.getResources().getConfiguration().orientation
+                != Configuration.ORIENTATION_PORTRAIT) {
+            // Orientation change did not occur on this device config. Skipping the test.
+            return;
+        }
 
         TestActivity.resetResumeCounter();
         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
@@ -189,9 +207,19 @@
         activity = mActivityTestRule.getActivity();
 
         activity.waitForLayout();
+        if (activity.getResources().getConfiguration().orientation
+                != Configuration.ORIENTATION_LANDSCAPE) {
+            // Orientation change did not occur on this device config. Skipping the test.
+            return;
+        }
 
-        verify(callbackInterface, atLeastOnce())
-                .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+        if (!isSidecar()) {
+            verify(callbackInterface, atLeastOnce())
+                    .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+        } else {
+            verify(callbackInterface, atLeastOnce())
+                    .onWindowLayoutChanged(any(), any());
+        }
     }
 
     @Test
@@ -212,6 +240,10 @@
                 || VERSION_0_1.equals(SidecarCompat.getSidecarVersion()));
     }
 
+    private boolean isSidecar() {
+        return SidecarCompat.getSidecarVersion() != null;
+    }
+
     /**
      * An argument matcher that ensures the arguments used to call are distinct.  The only exception
      * is to allow the first value to trigger twice in case the initial value is pushed and then
@@ -253,26 +285,38 @@
                 return true;
             }
 
-            for (DisplayFeature displayFeature :
-                    windowLayoutInfo.getDisplayFeatures()) {
-                int featureType = displayFeature.getType();
-                if (featureType != TYPE_FOLD && featureType != TYPE_HINGE) {
-                    return false;
-                }
-
-                Rect featureRect = displayFeature.getBounds();
-
-                if (featureRect.isEmpty() || featureRect.left < 0 || featureRect.top < 0) {
-                    return false;
-                }
-                if (featureRect.right < 1 || featureRect.right > mActivity.getWidth()) {
-                    return false;
-                }
-                if (featureRect.bottom < 1 || featureRect.bottom > mActivity.getHeight()) {
+            for (DisplayFeature displayFeature : windowLayoutInfo.getDisplayFeatures()) {
+                if (!isValid(mActivity, displayFeature)) {
                     return false;
                 }
             }
             return true;
         }
     }
+
+    private static boolean isValid(TestActivity activity, DisplayFeature displayFeature) {
+        if (!(displayFeature instanceof FoldingFeature)) {
+            return false;
+        }
+        FoldingFeature feature = (FoldingFeature) displayFeature;
+        int featureType = feature.getType();
+        if (featureType != FoldingFeature.TYPE_FOLD && featureType != FoldingFeature.TYPE_HINGE) {
+            return false;
+        }
+
+        Rect featureRect = feature.getBounds();
+        WindowMetrics windowMetrics = new WindowManager(activity).getCurrentWindowMetrics();
+
+        if ((featureRect.height() == 0 && featureRect.width() == 0) || featureRect.left < 0
+                || featureRect.top < 0) {
+            return false;
+        }
+        if (featureRect.right < 1 || featureRect.right > windowMetrics.getBounds().width()) {
+            return false;
+        }
+        if (featureRect.bottom < 1 || featureRect.bottom > windowMetrics.getBounds().height()) {
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java
index 09a9305..fee07ed 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java
@@ -30,6 +30,7 @@
 
 import androidx.window.extensions.ExtensionDeviceState;
 import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
 import androidx.window.extensions.ExtensionWindowLayoutInfo;
 
 import org.junit.After;
@@ -62,8 +63,8 @@
     public void testOnWindowLayoutChange_validFeature() {
         Activity mockActivity = mock(Activity.class);
         Rect bounds = new Rect(WINDOW_BOUNDS.left, 0, WINDOW_BOUNDS.right, 0);
-        ExtensionDisplayFeature foldFeature = new ExtensionDisplayFeature(bounds,
-                ExtensionDisplayFeature.TYPE_FOLD);
+        ExtensionDisplayFeature foldFeature = new ExtensionFoldingFeature(bounds,
+                ExtensionFoldingFeature.TYPE_FOLD, ExtensionFoldingFeature.STATE_FLAT);
 
         List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
         extensionDisplayFeatures.add(foldFeature);
@@ -71,7 +72,8 @@
                 new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
 
         List<DisplayFeature> expectedFeatures = new ArrayList<>();
-        expectedFeatures.add(new DisplayFeature(foldFeature.getBounds(), DisplayFeature.TYPE_FOLD));
+        expectedFeatures.add(new FoldingFeature(foldFeature.getBounds(), FoldingFeature.TYPE_FOLD,
+                FoldingFeature.STATE_FLAT));
         WindowLayoutInfo expected = new WindowLayoutInfo(expectedFeatures);
 
         ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
@@ -86,59 +88,16 @@
     }
 
     @Test
-    public void testOnWindowLayoutChange_filterRemovesEmptyBoundsFeature() {
-        List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
-        extensionDisplayFeatures.add(
-                new ExtensionDisplayFeature(new Rect(), ExtensionDisplayFeature.TYPE_FOLD));
-
-        ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
-        ExtensionTranslatingCallback extensionTranslatingCallback =
-                new ExtensionTranslatingCallback(mockCallback, new ExtensionAdapter());
-        ExtensionWindowLayoutInfo windowLayoutInfo =
-                new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
-        Activity mockActivity = mock(Activity.class);
-
-        extensionTranslatingCallback.onWindowLayoutChanged(mockActivity, windowLayoutInfo);
-
-        verify(mockCallback).onWindowLayoutChanged(eq(mockActivity),
-                argThat((layoutInfo) -> layoutInfo.getDisplayFeatures().isEmpty()));
-    }
-
-
-    @Test
-    public void testOnWindowLayoutChange_filterRemovesNonEmptyAreaFoldFeature() {
-        List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
-        Rect fullWidthBounds = new Rect(0, 1, WINDOW_BOUNDS.width(), 2);
-        Rect fullHeightBounds = new Rect(1, 0, 2, WINDOW_BOUNDS.height());
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
-                ExtensionDisplayFeature.TYPE_FOLD));
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
-                ExtensionDisplayFeature.TYPE_FOLD));
-
-        ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
-        ExtensionTranslatingCallback extensionTranslatingCallback =
-                new ExtensionTranslatingCallback(mockCallback, new ExtensionAdapter());
-        ExtensionWindowLayoutInfo windowLayoutInfo =
-                new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
-        Activity mockActivity = mock(Activity.class);
-
-        extensionTranslatingCallback.onWindowLayoutChanged(mockActivity, windowLayoutInfo);
-
-        verify(mockCallback).onWindowLayoutChanged(eq(mockActivity),
-                argThat((layoutInfo) -> layoutInfo.getDisplayFeatures().isEmpty()));
-    }
-
-    @Test
     public void testOnWindowLayoutChange_filterRemovesHingeFeatureNotSpanningFullDimension() {
         List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
         Rect fullWidthBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
                 WINDOW_BOUNDS.right / 2, 2);
         Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, 2,
                 WINDOW_BOUNDS.bottom / 2);
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
 
         ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
         ExtensionTranslatingCallback extensionTranslatingCallback =
@@ -161,10 +120,10 @@
                 WINDOW_BOUNDS.right / 2, WINDOW_BOUNDS.top);
         Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.left,
                 WINDOW_BOUNDS.bottom / 2);
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
-        extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
-                ExtensionDisplayFeature.TYPE_HINGE));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+        extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+                ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
 
         ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
         ExtensionTranslatingCallback extensionTranslatingCallback =
@@ -187,10 +146,6 @@
                 new ExtensionTranslatingCallback(mockCallback, new ExtensionAdapter());
 
         extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
-                ExtensionDeviceState.POSTURE_UNKNOWN));
-        extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
-                ExtensionDeviceState.POSTURE_CLOSED));
-        extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
                 ExtensionDeviceState.POSTURE_HALF_OPENED));
         extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
                 ExtensionDeviceState.POSTURE_OPENED));
@@ -201,11 +156,9 @@
         verify(mockCallback, atLeastOnce()).onDeviceStateChanged(captor.capture());
 
         List<DeviceState> values = captor.getAllValues();
-        assertEquals(DeviceState.POSTURE_UNKNOWN, values.get(0).getPosture());
-        assertEquals(DeviceState.POSTURE_CLOSED, values.get(1).getPosture());
-        assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(2).getPosture());
-        assertEquals(DeviceState.POSTURE_OPENED, values.get(3).getPosture());
-        assertEquals(DeviceState.POSTURE_FLIPPED, values.get(4).getPosture());
-        assertEquals(5, values.size());
+        assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(0).getPosture());
+        assertEquals(DeviceState.POSTURE_OPENED, values.get(1).getPosture());
+        assertEquals(DeviceState.POSTURE_FLIPPED, values.get(2).getPosture());
+        assertEquals(3, values.size());
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
index 2361ed1..97fbd20 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
@@ -52,6 +52,7 @@
 import java.util.List;
 
 /** Tests for {@link ExtensionWindowBackend} class. */
+@SuppressWarnings("deprecation") // TODO(b/173739071) remove DeviceState
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public final class ExtensionWindowBackendTest extends WindowTestBase {
@@ -132,7 +133,7 @@
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
 
         // Check registering the layout change callback
-        Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+        Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
         TestActivity activity = mActivityTestRule.launchActivity(new Intent());
         backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
 
@@ -152,10 +153,11 @@
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
 
         // Check registering the layout change callback
-        Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+        Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
         TestActivity activity = mActivityTestRule.launchActivity(new Intent());
         backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
-        backend.registerLayoutChangeCallback(activity, Runnable::run, mock(Consumer.class));
+        backend.registerLayoutChangeCallback(activity, Runnable::run,
+                mock(WindowLayoutInfoConsumer.class));
 
         assertEquals(2, backend.mWindowLayoutChangeCallbacks.size());
         verify(backend.mWindowExtension).onWindowLayoutChangeListenerAdded(activity);
@@ -174,8 +176,8 @@
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
 
         // Check registering the layout change callback
-        Consumer<WindowLayoutInfo> firstConsumer = mock(Consumer.class);
-        Consumer<WindowLayoutInfo> secondConsumer = mock(Consumer.class);
+        Consumer<WindowLayoutInfo> firstConsumer = mock(WindowLayoutInfoConsumer.class);
+        Consumer<WindowLayoutInfo> secondConsumer = mock(WindowLayoutInfoConsumer.class);
         TestActivity activity = mActivityTestRule.launchActivity(new Intent());
         backend.registerLayoutChangeCallback(activity, Runnable::run, firstConsumer);
         backend.registerLayoutChangeCallback(activity, Runnable::run, secondConsumer);
@@ -192,9 +194,10 @@
     public void testRegisterLayoutChangeCallback_relayLastEmittedValue() {
         WindowLayoutInfo expectedWindowLayoutInfo = newTestWindowLayoutInfo();
         ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+        Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
         TestActivity activity = mActivityTestRule.launchActivity(new Intent());
-        backend.registerLayoutChangeCallback(activity, Runnable::run, mock(Consumer.class));
+        backend.registerLayoutChangeCallback(activity, Runnable::run,
+                mock(WindowLayoutInfoConsumer.class));
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
         backend.mLastReportedWindowLayouts.put(activity, expectedWindowLayoutInfo);
 
@@ -209,7 +212,7 @@
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
 
         // Check that callbacks from the extension are propagated correctly
-        Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+        Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
         TestActivity activity = mActivityTestRule.launchActivity(new Intent());
 
         backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
@@ -230,13 +233,13 @@
 
     @Test
     public void testRegisterDeviceChangeCallback() {
-        DeviceState expectedState = newTestDeviceState();
-        ExtensionInterfaceCompat mockInterface = mock(ExtensionInterfaceCompat.class);
+        ExtensionInterfaceCompat mockInterface = mock(
+                ExtensionInterfaceCompat.class);
         ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
         backend.mWindowExtension = mockInterface;
 
         // Check registering the device state change callback
-        Consumer<DeviceState> consumer = mock(Consumer.class);
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
         backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
 
         assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
@@ -255,7 +258,7 @@
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
 
         // Check that callbacks from the extension are propagated correctly
-        Consumer<DeviceState> consumer = mock(Consumer.class);
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
 
         backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
         DeviceState deviceState = newTestDeviceState();
@@ -278,10 +281,10 @@
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
 
         // Check registering the layout change callback
-        Consumer<DeviceState> consumer = mock(Consumer.class);
-        TestActivity activity = mActivityTestRule.launchActivity(new Intent());
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+        mActivityTestRule.launchActivity(new Intent());
         backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-        backend.registerDeviceStateChangeCallback(Runnable::run, mock(Consumer.class));
+        backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
 
         assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
         verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
@@ -300,9 +303,9 @@
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
 
         // Check registering the layout change callback
-        Consumer<DeviceState> firstConsumer = mock(Consumer.class);
-        Consumer<DeviceState> secondConsumer = mock(Consumer.class);
-        TestActivity activity = mActivityTestRule.launchActivity(new Intent());
+        Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
+        Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
+        mActivityTestRule.launchActivity(new Intent());
         backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
         backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
 
@@ -318,7 +321,7 @@
     public void testDeviceChangeCallback_relayLastEmittedValue() {
         DeviceState expectedState = newTestDeviceState();
         ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        Consumer<DeviceState> consumer = mock(Consumer.class);
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
         backend.mLastReportedDeviceState = expectedState;
 
@@ -330,7 +333,7 @@
     @Test
     public void testDeviceChangeCallback_clearLastEmittedValue() {
         ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        Consumer<DeviceState> consumer = mock(Consumer.class);
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
 
         backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
         backend.unregisterDeviceStateChangeCallback(consumer);
@@ -345,14 +348,10 @@
 
         assertTrue(windowLayoutInfo.getDisplayFeatures().isEmpty());
 
-        DisplayFeature.Builder featureBuilder = new DisplayFeature.Builder();
-        featureBuilder.setType(DisplayFeature.TYPE_HINGE);
-        featureBuilder.setBounds(new Rect(0, 2, 3, 4));
-        DisplayFeature feature1 = featureBuilder.build();
-
-        featureBuilder = new DisplayFeature.Builder();
-        featureBuilder.setBounds(new Rect(0, 1, 5, 1));
-        DisplayFeature feature2 = featureBuilder.build();
+        DisplayFeature feature1 = new FoldingFeature(new Rect(0, 2, 3, 4),
+                FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT);
+        DisplayFeature feature2 = new FoldingFeature(new Rect(0, 1, 5, 1),
+                FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT);
 
         List<DisplayFeature> displayFeatures = new ArrayList<>();
         displayFeatures.add(feature1);
@@ -369,8 +368,12 @@
         return builder.build();
     }
 
+    private interface DeviceStateConsumer extends Consumer<DeviceState> { }
+
+    private interface WindowLayoutInfoConsumer extends Consumer<WindowLayoutInfo> { }
+
     private static class SimpleConsumer<T> implements Consumer<T> {
-        private List<T> mValues;
+        private final List<T> mValues;
 
         SimpleConsumer() {
             mValues = new ArrayList<>();
diff --git a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
new file mode 100644
index 0000000..9339492
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import static androidx.window.FoldingFeature.STATE_FLAT;
+import static androidx.window.FoldingFeature.STATE_FLIPPED;
+import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
+import static androidx.window.FoldingFeature.TYPE_FOLD;
+import static androidx.window.FoldingFeature.TYPE_HINGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link FoldingFeature} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class FoldingFeatureTest {
+
+    @Test(expected = IllegalArgumentException.class)
+    public void tesEmptyRect() {
+        new FoldingFeature(new Rect(), TYPE_HINGE, STATE_HALF_OPENED);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testFoldWithNonZeroArea() {
+        new FoldingFeature(new Rect(0, 0, 20, 30), TYPE_FOLD, STATE_FLIPPED);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testHorizontalHingeWithNonZeroOrigin() {
+        new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_HINGE, STATE_FLIPPED);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testVerticalHingeWithNonZeroOrigin() {
+        new FoldingFeature(new Rect(10, 1, 19, 29), TYPE_HINGE, STATE_FLIPPED);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testHorizontalFoldWithNonZeroOrigin() {
+        new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_FOLD, STATE_FLIPPED);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testVerticalFoldWithNonZeroOrigin() {
+        new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_FLIPPED);
+    }
+
+    @Test
+    public void testSetBoundsAndType() {
+        Rect bounds = new Rect(0, 10, 30, 10);
+        int type = TYPE_HINGE;
+        int state = STATE_HALF_OPENED;
+        FoldingFeature feature = new FoldingFeature(bounds, type, state);
+
+        assertEquals(bounds, feature.getBounds());
+        assertEquals(type, feature.getType());
+        assertEquals(state, feature.getState());
+    }
+
+    @Test
+    public void testEquals_sameAttributes() {
+        Rect bounds = new Rect(1, 0, 1, 10);
+        int type = TYPE_FOLD;
+        int state = STATE_FLAT;
+
+        FoldingFeature original = new FoldingFeature(bounds, type, state);
+        FoldingFeature copy = new FoldingFeature(bounds, type, state);
+
+        assertEquals(original, copy);
+    }
+
+    @Test
+    public void testEquals_differentRect() {
+        Rect originalRect = new Rect(1, 0, 1, 10);
+        Rect otherRect = new Rect(2, 0, 2, 10);
+        int type = TYPE_FOLD;
+        int state = STATE_FLAT;
+
+        FoldingFeature original = new FoldingFeature(originalRect, type, state);
+        FoldingFeature other = new FoldingFeature(otherRect, type, state);
+
+        assertNotEquals(original, other);
+    }
+
+    @Test
+    public void testEquals_differentType() {
+        Rect rect = new Rect(1, 0, 1, 10);
+        int originalType = TYPE_FOLD;
+        int otherType = TYPE_HINGE;
+        int state = STATE_FLAT;
+
+        FoldingFeature original = new FoldingFeature(rect, originalType, state);
+        FoldingFeature other = new FoldingFeature(rect, otherType, state);
+
+        assertNotEquals(original, other);
+    }
+
+    @Test
+    public void testEquals_differentState() {
+        Rect rect = new Rect(1, 0, 1, 10);
+        int type = TYPE_FOLD;
+        int originalState = STATE_FLAT;
+        int otherState = STATE_FLIPPED;
+
+        FoldingFeature original = new FoldingFeature(rect, type, originalState);
+        FoldingFeature other = new FoldingFeature(rect, type, otherState);
+
+        assertNotEquals(original, other);
+    }
+
+    @Test
+    public void testHashCode_matchesIfEqual() {
+        Rect originalRect = new Rect(1, 0, 1, 10);
+        Rect matchingRect = new Rect(1, 0, 1, 10);
+        int type = TYPE_FOLD;
+        int state = STATE_FLAT;
+
+        FoldingFeature original = new FoldingFeature(originalRect, type, state);
+        FoldingFeature matching = new FoldingFeature(matchingRect, type, state);
+
+        assertEquals(original, matching);
+        assertEquals(original.hashCode(), matching.hashCode());
+    }
+}
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java b/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java
index 65e2903..8272c3c 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java
@@ -16,6 +16,9 @@
 
 package androidx.window;
 
+import static androidx.window.SidecarAdapter.setSidecarDevicePosture;
+import static androidx.window.SidecarAdapter.setSidecarDisplayFeatures;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
@@ -62,14 +65,13 @@
     private static SidecarWindowLayoutInfo sidecarWindowLayoutInfo(
             List<SidecarDisplayFeature> features) {
         SidecarWindowLayoutInfo layoutInfo = new SidecarWindowLayoutInfo();
-        layoutInfo.displayFeatures = new ArrayList<>();
-        layoutInfo.displayFeatures.addAll(features);
+        setSidecarDisplayFeatures(layoutInfo, features);
         return layoutInfo;
     }
 
     private static SidecarDeviceState sidecarDeviceState(int posture) {
         SidecarDeviceState deviceState = new SidecarDeviceState();
-        deviceState.posture = posture;
+        setSidecarDevicePosture(deviceState, posture);
         return deviceState;
     }
 
@@ -85,19 +87,21 @@
         sidecarDisplayFeatures.add(foldFeature);
         SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
 
+        SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
+
         List<DisplayFeature> expectedFeatures = new ArrayList<>();
-        expectedFeatures.add(new DisplayFeature(foldFeature.getRect(), DisplayFeature.TYPE_FOLD));
+        expectedFeatures.add(new FoldingFeature(foldFeature.getRect(), FoldingFeature.TYPE_FOLD,
+                FoldingFeature.STATE_FLAT));
         WindowLayoutInfo expected = new WindowLayoutInfo(expectedFeatures);
 
         SidecarAdapter sidecarAdapter = new SidecarAdapter();
 
-        WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo);
+        WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo, state);
 
         assertEquals(expected, actual);
     }
 
     @Test
-    @Override
     public void testTranslateWindowLayoutInfo_filterRemovesEmptyBoundsFeature() {
         List<SidecarDisplayFeature> sidecarDisplayFeatures = new ArrayList<>();
         sidecarDisplayFeatures.add(
@@ -106,15 +110,15 @@
         SidecarAdapter sidecarAdapter = new SidecarAdapter();
         SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
         Activity mockActivity = mock(Activity.class);
+        SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
 
-        WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo);
+        WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo, state);
 
         assertTrue(actual.getDisplayFeatures().isEmpty());
     }
 
 
     @Test
-    @Override
     public void testTranslateWindowLayoutInfo_filterRemovesNonEmptyAreaFoldFeature() {
         List<SidecarDisplayFeature> sidecarDisplayFeatures = new ArrayList<>();
         Rect fullWidthBounds = new Rect(0, 1, WINDOW_BOUNDS.width(), 2);
@@ -130,7 +134,10 @@
         SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
         Activity mockActivity = mock(Activity.class);
 
-        WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo);
+        SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
+
+        WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo,
+                state);
 
         assertTrue(actual.getDisplayFeatures().isEmpty());
     }
@@ -151,10 +158,11 @@
 
         SidecarAdapter sidecarAdapter = new SidecarAdapter();
         SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
+        SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
 
         Activity mockActivity = mock(Activity.class);
 
-        WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo);
+        WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo, state);
 
         assertTrue(actual.getDisplayFeatures().isEmpty());
     }
@@ -175,10 +183,12 @@
         SidecarAdapter sidecarCallbackAdapter = new SidecarAdapter();
         SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(
                 extensionDisplayFeatures);
+        SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
 
         Activity mockActivity = mock(Activity.class);
 
-        WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo);
+        WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo,
+                state);
 
         assertTrue(actual.getDisplayFeatures().isEmpty());
     }
@@ -186,8 +196,6 @@
     @Test
     @Override
     public void testTranslateDeviceState() {
-        ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
-                ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
         SidecarAdapter sidecarCallbackAdapter = new SidecarAdapter();
         List<DeviceState> values = new ArrayList<>();
 
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java
index 954ed4a..cfc6286 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java
@@ -17,12 +17,15 @@
 package androidx.window;
 
 import static androidx.window.ExtensionInterfaceCompat.ExtensionCallbackInterface;
+import static androidx.window.SidecarAdapter.getSidecarDevicePosture;
+import static androidx.window.SidecarAdapter.getSidecarDisplayFeatures;
 import static androidx.window.Version.VERSION_0_1;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
@@ -42,6 +45,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentMatcher;
 
+import java.util.List;
+
 /**
  * Tests for {@link SidecarCompat} implementation of {@link ExtensionInterfaceCompat} that are
  * executed with Sidecar implementation provided on the device (and only if one is available).
@@ -66,8 +71,8 @@
         mSidecarCompat.onDeviceStateListenersChanged(false);
 
 
-        verify(callbackInterface).onDeviceStateChanged(argThat(
-                deviceState -> deviceState.getPosture() == sidecarDeviceState.posture));
+        verify(callbackInterface, atLeastOnce()).onDeviceStateChanged(argThat(deviceState ->
+                deviceState.getPosture() == getSidecarDevicePosture(sidecarDeviceState)));
     }
 
     @Test
@@ -83,7 +88,7 @@
         SidecarWindowLayoutInfo sidecarWindowLayoutInfo =
                 mSidecarCompat.mSidecar.getWindowLayoutInfo(windowToken);
 
-        verify(callbackInterface).onWindowLayoutChanged(any(),
+        verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(any(),
                 argThat(new SidecarMatcher(sidecarWindowLayoutInfo)));
     }
 
@@ -102,14 +107,16 @@
 
         @Override
         public boolean matches(WindowLayoutInfo windowLayoutInfo) {
-            if (windowLayoutInfo.getDisplayFeatures().size()
-                    != mSidecarWindowLayoutInfo.displayFeatures.size()) {
+            List<SidecarDisplayFeature> sidecarDisplayFeatures =
+                    getSidecarDisplayFeatures(mSidecarWindowLayoutInfo);
+            if (windowLayoutInfo.getDisplayFeatures().size() != sidecarDisplayFeatures.size()) {
                 return false;
             }
             for (int i = 0; i < windowLayoutInfo.getDisplayFeatures().size(); i++) {
-                DisplayFeature feature = windowLayoutInfo.getDisplayFeatures().get(i);
-                SidecarDisplayFeature sidecarDisplayFeature =
-                        mSidecarWindowLayoutInfo.displayFeatures.get(i);
+                // Sidecar only has folding features
+                FoldingFeature feature = (FoldingFeature) windowLayoutInfo.getDisplayFeatures()
+                        .get(i);
+                SidecarDisplayFeature sidecarDisplayFeature = sidecarDisplayFeatures.get(i);
 
                 if (feature.getType() != sidecarDisplayFeature.getType()) {
                     return false;
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
index 05f6c7d..5acba49 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
@@ -16,16 +16,23 @@
 
 package androidx.window;
 
-import static androidx.window.TestBoundUtil.invalidFoldBounds;
-import static androidx.window.TestBoundUtil.invalidHingeBounds;
-import static androidx.window.TestBoundUtil.validFoldBound;
+import static androidx.window.SidecarAdapter.getSidecarDisplayFeatures;
+import static androidx.window.SidecarAdapter.setSidecarDevicePosture;
+import static androidx.window.SidecarAdapter.setSidecarDisplayFeatures;
+import static androidx.window.TestBoundsUtil.invalidFoldBounds;
+import static androidx.window.TestBoundsUtil.invalidHingeBounds;
+import static androidx.window.TestBoundsUtil.validFoldBound;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -54,6 +61,7 @@
 import org.mockito.ArgumentCaptor;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -89,14 +97,15 @@
 
         // Setup mocked sidecar responses
         SidecarDeviceState defaultDeviceState = new SidecarDeviceState();
-        defaultDeviceState.posture = SidecarDeviceState.POSTURE_HALF_OPENED;
+        setSidecarDevicePosture(defaultDeviceState, SidecarDeviceState.POSTURE_HALF_OPENED);
         when(mSidecarCompat.mSidecar.getDeviceState()).thenReturn(defaultDeviceState);
 
         SidecarDisplayFeature sidecarDisplayFeature = newDisplayFeature(
                 new Rect(0, 1, WINDOW_BOUNDS.width(), 1), SidecarDisplayFeature.TYPE_HINGE);
         SidecarWindowLayoutInfo sidecarWindowLayoutInfo = new SidecarWindowLayoutInfo();
-        sidecarWindowLayoutInfo.displayFeatures = new ArrayList<>();
-        sidecarWindowLayoutInfo.displayFeatures.add(sidecarDisplayFeature);
+        List<SidecarDisplayFeature> displayFeatures = new ArrayList<>();
+        displayFeatures.add(sidecarDisplayFeature);
+        setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures);
         when(mSidecarCompat.mSidecar.getWindowLayoutInfo(any()))
                 .thenReturn(sidecarWindowLayoutInfo);
     }
@@ -109,23 +118,27 @@
     @Test
     @Override
     public void testGetDeviceState() {
-        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+                newDeviceState(SidecarDeviceState.POSTURE_OPENED),
+                newWindowLayoutInfo(Collections.emptyList()));
         SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
         ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
                 ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
         compat.setExtensionCallback(mockCallback);
         compat.onDeviceStateListenersChanged(false);
-        SidecarDeviceState deviceState = newDeviceState(SidecarDeviceState.POSTURE_OPENED);
+        SidecarDeviceState deviceState = newDeviceState(SidecarDeviceState.POSTURE_HALF_OPENED);
 
         fakeSidecarImp.triggerDeviceState(deviceState);
 
-        verify(mockCallback).onDeviceStateChanged(any());
+        verify(mockCallback, atLeastOnce()).onDeviceStateChanged(any());
     }
 
     @Test
     @Override
     public void testGetWindowLayout() {
-        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+                newDeviceState(SidecarDeviceState.POSTURE_OPENED),
+                newWindowLayoutInfo(Collections.emptyList()));
         SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
         ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
                 ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
@@ -134,7 +147,8 @@
 
         fakeSidecarImp.triggerGoodSignal();
 
-        verify(mockCallback).onWindowLayoutChanged(any(), any());
+        verify(mockCallback, atLeastOnce()).onWindowLayoutChanged(eq(mActivity),
+                any(WindowLayoutInfo.class));
     }
 
     @Test
@@ -143,7 +157,7 @@
         SidecarWindowLayoutInfo originalWindowLayoutInfo =
                 mSidecarCompat.mSidecar.getWindowLayoutInfo(getActivityWindowToken(mActivity));
         List<SidecarDisplayFeature> sidecarDisplayFeatures =
-                originalWindowLayoutInfo.displayFeatures;
+                getSidecarDisplayFeatures(originalWindowLayoutInfo);
         SidecarDisplayFeature newFeature = new SidecarDisplayFeature();
         newFeature.setRect(new Rect());
         sidecarDisplayFeatures.add(newFeature);
@@ -160,7 +174,7 @@
         SidecarWindowLayoutInfo originalWindowLayoutInfo =
                 mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
         List<SidecarDisplayFeature> sidecarDisplayFeatures =
-                originalWindowLayoutInfo.displayFeatures;
+                getSidecarDisplayFeatures(originalWindowLayoutInfo);
         // Horizontal fold.
         sidecarDisplayFeatures.add(
                 newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width(), 2),
@@ -183,7 +197,7 @@
         SidecarWindowLayoutInfo originalWindowLayoutInfo =
                 mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
         List<SidecarDisplayFeature> sidecarDisplayFeatures =
-                originalWindowLayoutInfo.displayFeatures;
+                getSidecarDisplayFeatures(originalWindowLayoutInfo);
         // Horizontal hinge.
         sidecarDisplayFeatures.add(
                 newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
@@ -206,7 +220,7 @@
         SidecarWindowLayoutInfo originalWindowLayoutInfo =
                 mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
         List<SidecarDisplayFeature> sidecarDisplayFeatures =
-                originalWindowLayoutInfo.displayFeatures;
+                getSidecarDisplayFeatures(originalWindowLayoutInfo);
         // Horizontal fold.
         sidecarDisplayFeatures.add(
                 newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
@@ -226,7 +240,9 @@
 
     @Override
     public void testExtensionCallback_filterRemovesInvalidValues() {
-        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+                newDeviceState(SidecarDeviceState.POSTURE_OPENED),
+                newWindowLayoutInfo(Collections.emptyList()));
         SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
         ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
                 ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
@@ -254,7 +270,7 @@
 
         // Verify that the callback set for sidecar propagates the device state callback
         SidecarDeviceState sidecarDeviceState = new SidecarDeviceState();
-        sidecarDeviceState.posture = SidecarDeviceState.POSTURE_HALF_OPENED;
+        setSidecarDevicePosture(sidecarDeviceState, SidecarDeviceState.POSTURE_HALF_OPENED);
 
         sidecarCallbackCaptor.getValue().onDeviceStateChanged(sidecarDeviceState);
         ArgumentCaptor<DeviceState> deviceStateCaptor = ArgumentCaptor.forClass(DeviceState.class);
@@ -268,8 +284,9 @@
         SidecarDisplayFeature sidecarDisplayFeature = newDisplayFeature(bounds,
                 SidecarDisplayFeature.TYPE_HINGE);
         SidecarWindowLayoutInfo sidecarWindowLayoutInfo = new SidecarWindowLayoutInfo();
-        sidecarWindowLayoutInfo.displayFeatures = new ArrayList<>();
-        sidecarWindowLayoutInfo.displayFeatures.add(sidecarDisplayFeature);
+        List<SidecarDisplayFeature> displayFeatures = new ArrayList<>();
+        displayFeatures.add(sidecarDisplayFeature);
+        setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures);
 
         sidecarCallbackCaptor.getValue().onWindowLayoutChanged(getActivityWindowToken(mActivity),
                 sidecarWindowLayoutInfo);
@@ -281,7 +298,9 @@
         WindowLayoutInfo capturedLayout = windowLayoutInfoCaptor.getValue();
         assertEquals(1, capturedLayout.getDisplayFeatures().size());
         DisplayFeature capturedDisplayFeature = capturedLayout.getDisplayFeatures().get(0);
-        assertEquals(DisplayFeature.TYPE_HINGE, capturedDisplayFeature.getType());
+        FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
+        assertNotNull(foldingFeature);
+        assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
         assertEquals(bounds, capturedDisplayFeature.getBounds());
     }
 
@@ -304,8 +323,9 @@
         Rect bounds = new Rect(1, 2, 3, 4);
         sidecarDisplayFeature.setRect(bounds);
         SidecarWindowLayoutInfo sidecarWindowLayoutInfo = new SidecarWindowLayoutInfo();
-        sidecarWindowLayoutInfo.displayFeatures = new ArrayList<>();
-        sidecarWindowLayoutInfo.displayFeatures.add(sidecarDisplayFeature);
+        List<SidecarDisplayFeature> displayFeatures = new ArrayList<>();
+        displayFeatures.add(sidecarDisplayFeature);
+        setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures);
 
         IBinder windowToken = mock(IBinder.class);
         sidecarCallbackCaptor.getValue().onWindowLayoutChanged(windowToken,
@@ -345,9 +365,8 @@
         when(mSidecarCompat.mSidecar.getWindowLayoutInfo(any())).thenReturn(layoutInfo);
         View fakeView = mock(View.class);
         doAnswer(invocation -> {
-            View.OnAttachStateChangeListener stateChangeListener =
-                    (View.OnAttachStateChangeListener) invocation.getArgument(0);
-            stateChangeListener.onViewAttachedToWindow((View) invocation.getMock());
+            View.OnAttachStateChangeListener stateChangeListener = invocation.getArgument(0);
+            stateChangeListener.onViewAttachedToWindow(fakeView);
             return null;
         }).when(fakeView).addOnAttachStateChangeListener(any());
         Window fakeWindow = new TestWindow(mActivity, fakeView);
@@ -387,6 +406,47 @@
         verify(listener).onDeviceStateChanged(expectedDeviceState);
     }
 
+    @Test
+    public void testOnDeviceStateChangedUpdatesWindowLayout() {
+        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+                newDeviceState(SidecarDeviceState.POSTURE_CLOSED),
+                validWindowLayoutInfo());
+        SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
+        ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
+                ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+        compat.setExtensionCallback(mockCallback);
+        compat.onWindowLayoutChangeListenerAdded(mActivity);
+        ArgumentCaptor<WindowLayoutInfo> windowLayoutCaptor = ArgumentCaptor.forClass(
+                WindowLayoutInfo.class);
+
+        reset(mockCallback);
+        fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_OPENED));
+        verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+        FoldingFeature capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue()
+                .getDisplayFeatures().get(0);
+        assertEquals(FoldingFeature.STATE_FLAT, capturedFoldingFeature.getState());
+
+        reset(mockCallback);
+        fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_HALF_OPENED));
+        verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+        capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue().getDisplayFeatures()
+                .get(0);
+        assertEquals(FoldingFeature.STATE_HALF_OPENED, capturedFoldingFeature.getState());
+
+        reset(mockCallback);
+        fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_FLIPPED));
+        verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+        capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue().getDisplayFeatures()
+                .get(0);
+        assertEquals(FoldingFeature.STATE_FLIPPED, capturedFoldingFeature.getState());
+
+        // No display features must be reported in closed state
+        reset(mockCallback);
+        fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_CLOSED));
+        verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+        assertTrue(windowLayoutCaptor.getValue().getDisplayFeatures().isEmpty());
+    }
+
     private static SidecarDisplayFeature newDisplayFeature(Rect rect, int type) {
         SidecarDisplayFeature feature = new SidecarDisplayFeature();
         feature.setRect(rect);
@@ -397,23 +457,36 @@
     private static SidecarWindowLayoutInfo newWindowLayoutInfo(
             List<SidecarDisplayFeature> features) {
         SidecarWindowLayoutInfo info = new SidecarWindowLayoutInfo();
-        info.displayFeatures = new ArrayList<>();
-        info.displayFeatures.addAll(features);
+        setSidecarDisplayFeatures(info, features);
         return info;
     }
 
+    private static SidecarWindowLayoutInfo validWindowLayoutInfo() {
+        List<SidecarDisplayFeature> goodFeatures = new ArrayList<>();
+
+        goodFeatures.add(newDisplayFeature(validFoldBound(WINDOW_BOUNDS),
+                SidecarDisplayFeature.TYPE_FOLD));
+
+        return newWindowLayoutInfo(goodFeatures);
+    }
+
     private static SidecarDeviceState newDeviceState(int posture) {
         SidecarDeviceState state = new SidecarDeviceState();
-        state.posture = posture;
+        setSidecarDevicePosture(state, posture);
         return state;
     }
 
     private static final class FakeExtensionImp implements SidecarInterface {
 
+        private SidecarDeviceState mDeviceState;
+        private SidecarWindowLayoutInfo mInfo;
         private SidecarInterface.SidecarCallback mCallback;
         private List<IBinder> mTokens = new ArrayList<>();
 
-        FakeExtensionImp() {
+        FakeExtensionImp(@NonNull SidecarDeviceState deviceState,
+                @NonNull SidecarWindowLayoutInfo info) {
+            mDeviceState = deviceState;
+            mInfo = info;
             mCallback = new SidecarInterface.SidecarCallback() {
                 @Override
                 public void onDeviceStateChanged(@NonNull SidecarDeviceState newDeviceState) {
@@ -430,29 +503,29 @@
 
         @Override
         public void setSidecarCallback(@NonNull SidecarCallback callback) {
-
+            mCallback = callback;
         }
 
         @NonNull
         @Override
         public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
-            return null;
+            return mInfo;
         }
 
         @Override
         public void onWindowLayoutChangeListenerAdded(@NonNull IBinder windowToken) {
-
+            mTokens.add(windowToken);
         }
 
         @Override
         public void onWindowLayoutChangeListenerRemoved(@NonNull IBinder windowToken) {
-
+            mTokens.remove(windowToken);
         }
 
         @NonNull
         @Override
         public SidecarDeviceState getDeviceState() {
-            return null;
+            return mDeviceState;
         }
 
         @Override
@@ -469,14 +542,15 @@
         }
 
         void triggerSignal(SidecarWindowLayoutInfo info) {
-            for (IBinder token: mTokens) {
+            mInfo = info;
+            for (IBinder token : mTokens) {
                 mCallback.onWindowLayoutChanged(token, info);
             }
         }
 
         public void triggerDeviceState(SidecarDeviceState state) {
+            mDeviceState = state;
             mCallback.onDeviceStateChanged(state);
-
         }
 
         private SidecarWindowLayoutInfo malformedWindowLayoutInfo() {
@@ -494,14 +568,5 @@
 
             return newWindowLayoutInfo(malformedFeatures);
         }
-
-        private SidecarWindowLayoutInfo validWindowLayoutInfo() {
-            List<SidecarDisplayFeature> goodFeatures = new ArrayList<>();
-
-            goodFeatures.add(newDisplayFeature(validFoldBound(WINDOW_BOUNDS),
-                    SidecarDisplayFeature.TYPE_FOLD));
-
-            return newWindowLayoutInfo(goodFeatures);
-        }
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/TestActivity.java b/window/window/src/androidTest/java/androidx/window/TestActivity.java
index ee73aac..65d1a81 100644
--- a/window/window/src/androidTest/java/androidx/window/TestActivity.java
+++ b/window/window/src/androidTest/java/androidx/window/TestActivity.java
@@ -28,7 +28,7 @@
 public class TestActivity extends Activity implements View.OnLayoutChangeListener {
 
     private int mRootViewId;
-    private CountDownLatch mLayoutLatch;
+    private CountDownLatch mLayoutLatch = new CountDownLatch(1);
     private static CountDownLatch sResumeLatch = new CountDownLatch(1);
 
     @Override
@@ -39,18 +39,9 @@
         contentView.setId(mRootViewId);
         setContentView(contentView);
 
-        resetLayoutCounter();
         getWindow().getDecorView().addOnLayoutChangeListener(this);
     }
 
-    int getWidth() {
-        return findViewById(mRootViewId).getWidth();
-    }
-
-    int getHeight() {
-        return findViewById(mRootViewId).getHeight();
-    }
-
     @Override
     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
             int oldTop, int oldRight, int oldBottom) {
diff --git a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java b/window/window/src/androidTest/java/androidx/window/TestBoundsUtil.java
similarity index 79%
copy from window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
copy to window/window/src/androidTest/java/androidx/window/TestBoundsUtil.java
index 18447f5..ff353db 100644
--- a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
+++ b/window/window/src/androidTest/java/androidx/window/TestBoundsUtil.java
@@ -24,10 +24,11 @@
 /**
  * A utility class to provide bounds for a display feature
  */
-class TestBoundUtil {
+class TestBoundsUtil {
 
     public static Rect validFoldBound(Rect windowBounds) {
-        return new Rect(windowBounds.left, windowBounds.top, windowBounds.right, 0);
+        int verticalMid = windowBounds.top + windowBounds.height() / 2;
+        return new Rect(0, verticalMid, windowBounds.width(), verticalMid);
     }
 
     public static Rect invalidZeroBound() {
@@ -35,11 +36,11 @@
     }
 
     public static Rect invalidBoundShortWidth(Rect windowBounds) {
-        return new Rect(windowBounds.left, windowBounds.top, windowBounds.right / 2, 2);
+        return new Rect(0, 0, windowBounds.width() / 2, 2);
     }
 
-    public static Rect invalidBoundShortHeightHeight(Rect windowBounds) {
-        return new Rect(windowBounds.left, windowBounds.top, 2, windowBounds.bottom / 2);
+    public static Rect invalidBoundShortHeight(Rect windowBounds) {
+        return new Rect(0, 0, 2, windowBounds.height() / 2);
     }
 
     private static List<Rect> coreInvalidBounds(Rect windowBounds) {
@@ -47,7 +48,7 @@
 
         badBounds.add(invalidZeroBound());
         badBounds.add(invalidBoundShortWidth(windowBounds));
-        badBounds.add(invalidBoundShortHeightHeight(windowBounds));
+        badBounds.add(invalidBoundShortHeight(windowBounds));
 
         return badBounds;
     }
diff --git a/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java b/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java
index 90eb92b..958063b 100644
--- a/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java
+++ b/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java
@@ -23,8 +23,6 @@
 public interface TranslatorTestInterface {
     void testTranslate_validFeature();
     void testTranslateDeviceState();
-    void testTranslateWindowLayoutInfo_filterRemovesEmptyBoundsFeature();
-    void testTranslateWindowLayoutInfo_filterRemovesNonEmptyAreaFoldFeature();
     void testTranslateWindowLayoutInfo_filterRemovesHingeFeatureNotSpanningFullDimension();
     void testTranslateWindowLayoutInfo_filterRemovesFoldFeatureNotSpanningFullDimension();
 }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
index 6378d7a..d807de5 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
@@ -65,8 +65,9 @@
 
     private WindowLayoutInfo newTestWindowLayout() {
         List<DisplayFeature> displayFeatureList = new ArrayList<>();
-        DisplayFeature displayFeature = new DisplayFeature(
-                new Rect(10, 0, 10, 100), DisplayFeature.TYPE_HINGE);
+        DisplayFeature displayFeature = new FoldingFeature(
+                new Rect(10, 0, 10, 100), FoldingFeature.TYPE_HINGE,
+                FoldingFeature.STATE_FLAT);
         displayFeatureList.add(displayFeature);
         return new WindowLayoutInfo(displayFeatureList);
     }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
index cf6eab3..5db5039 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
@@ -33,6 +33,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 
+import org.junit.AssumptionViolatedException;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -48,10 +49,9 @@
     @Test
     public void testGetCurrentWindowBounds_matchParentWindowSize_avoidCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = WindowManager.LayoutParams.MATCH_PARENT;
             lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -66,10 +66,9 @@
     @Test
     public void testGetCurrentWindowBounds_fixedWindowSize_avoidCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = 100;
             lp.height = 100;
@@ -84,10 +83,9 @@
     @Test
     public void testGetCurrentWindowBounds_matchParentWindowSize_layoutBehindCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = WindowManager.LayoutParams.MATCH_PARENT;
             lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -102,10 +100,9 @@
     @Test
     public void testGetCurrentWindowBounds_fixedWindowSize_layoutBehindCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = 100;
             lp.height = 100;
@@ -132,10 +129,9 @@
     @Test
     public void testGetMaximumWindowBounds_matchParentWindowSize_avoidCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = WindowManager.LayoutParams.MATCH_PARENT;
             lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -150,10 +146,9 @@
     @Test
     public void testGetMaximumWindowBounds_fixedWindowSize_avoidCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = 100;
             lp.height = 100;
@@ -168,10 +163,9 @@
     @Test
     public void testGetMaximumWindowBounds_matchParentWindowSize_layoutBehindCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = WindowManager.LayoutParams.MATCH_PARENT;
             lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -186,10 +180,9 @@
     @Test
     public void testGetMaximumWindowBounds_fixedWindowSize_layoutBehindCutouts_preR() {
         assumePlatformBeforeR();
+        assumeNotMultiWindow();
 
         testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
-            assumeFalse(isInMultiWindowMode(activity));
-
             WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
             lp.width = 100;
             lp.height = 100;
@@ -250,6 +243,20 @@
         scenario.onActivity(verifyAction);
     }
 
+    private void assumeNotMultiWindow() {
+        ActivityScenario<TestActivity> scenario = mActivityScenarioRule.getScenario();
+        try {
+            scenario.onActivity(activity -> assumeFalse(isInMultiWindowMode(activity)));
+        } catch (RuntimeException e) {
+            if (e.getCause() instanceof AssumptionViolatedException) {
+                AssumptionViolatedException failedAssumption =
+                        (AssumptionViolatedException) e.getCause();
+                throw failedAssumption;
+            }
+            throw e;
+        }
+    }
+
     private static boolean isInMultiWindowMode(Activity activity) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             return activity.isInMultiWindowMode();
diff --git a/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java b/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java
index 8c97849..62154b1 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java
@@ -48,14 +48,11 @@
 
     @Test
     public void testBuilder_setDisplayFeatures() {
-        DisplayFeature.Builder featureBuilder = new DisplayFeature.Builder();
-        featureBuilder.setType(DisplayFeature.TYPE_HINGE);
-        featureBuilder.setBounds(new Rect(1, 0, 3, 4));
-        DisplayFeature feature1 = featureBuilder.build();
+        DisplayFeature feature1 = new FoldingFeature(new Rect(1, 0, 3, 4),
+                FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT);
 
-        featureBuilder = new DisplayFeature.Builder();
-        featureBuilder.setBounds(new Rect(1, 0, 1, 4));
-        DisplayFeature feature2 = featureBuilder.build();
+        DisplayFeature feature2 = new FoldingFeature(new Rect(1, 0, 1, 4),
+                FoldingFeature.STATE_FLAT, FoldingFeature.STATE_FLAT);
 
         List<DisplayFeature> displayFeatures = new ArrayList<>();
         displayFeatures.add(feature1);
@@ -83,7 +80,8 @@
         List<DisplayFeature> originalFeatures = new ArrayList<>();
         List<DisplayFeature> differentFeatures = new ArrayList<>();
         Rect rect = new Rect(1, 0, 1, 10);
-        differentFeatures.add(new DisplayFeature(rect, 1));
+        differentFeatures.add(new FoldingFeature(rect, FoldingFeature.TYPE_HINGE,
+                FoldingFeature.STATE_FLAT));
 
         WindowLayoutInfo original = new WindowLayoutInfo(originalFeatures);
         WindowLayoutInfo different = new WindowLayoutInfo(differentFeatures);
@@ -104,13 +102,15 @@
 
     @Test
     public void testHashCode_matchesIfEqualFeatures() {
-        DisplayFeature originalFeature = new DisplayFeature(
-                new Rect(-1, 1, 1, -1),
-                0
+        DisplayFeature originalFeature = new FoldingFeature(
+                new Rect(0, 0, 100, 0),
+                FoldingFeature.TYPE_HINGE,
+                FoldingFeature.STATE_FLAT
         );
-        DisplayFeature matchingFeature = new DisplayFeature(
-                new Rect(-1, 1, 1, -1),
-                0
+        DisplayFeature matchingFeature = new FoldingFeature(
+                new Rect(0, 0, 100, 0),
+                FoldingFeature.TYPE_HINGE,
+                FoldingFeature.STATE_FLAT
         );
         List<DisplayFeature> firstFeatures = Collections.singletonList(originalFeature);
         List<DisplayFeature> secondFeatures = Collections.singletonList(matchingFeature);
diff --git a/window/window/src/main/java/androidx/window/DeviceState.java b/window/window/src/main/java/androidx/window/DeviceState.java
index d031629..5584f71 100644
--- a/window/window/src/main/java/androidx/window/DeviceState.java
+++ b/window/window/src/main/java/androidx/window/DeviceState.java
@@ -23,9 +23,12 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
+ * @deprecated Use {@link FoldingFeature} to get the state of the hinge instead. Will be removed in
+ * alpha03.
  * Information about the state of the device.
  * <p>Currently only includes the description of the state for foldable devices.
  */
+@Deprecated
 public final class DeviceState {
 
     @Posture
diff --git a/window/window/src/main/java/androidx/window/DisplayFeature.java b/window/window/src/main/java/androidx/window/DisplayFeature.java
index efa0ceb..09d9491 100644
--- a/window/window/src/main/java/androidx/window/DisplayFeature.java
+++ b/window/window/src/main/java/androidx/window/DisplayFeature.java
@@ -18,12 +18,8 @@
 
 import android.graphics.Rect;
 
-import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
 /**
  * Description of a physical feature on the display.
  *
@@ -31,162 +27,15 @@
  * the device. It can intrude into the application window space and create a visual distortion,
  * visual or touch discontinuity, make some area invisible or create a logical divider or separation
  * in the screen space.
- *
- * @see #TYPE_FOLD
- * @see #TYPE_HINGE
  */
-public final class DisplayFeature {
-    private final Rect mBounds;
-    @Type
-    private int mType;
-
-    DisplayFeature(@NonNull Rect bounds, @Type int type) {
-        assertValidBounds(bounds, type);
-        mBounds = new Rect(bounds);
-        mType = type;
-    }
+public interface DisplayFeature {
 
     /**
-     * Gets bounding rectangle of the physical display feature in the coordinate space of the
-     * window. The rectangle provides information about the location of the feature in the window
-     * and its size.
+     * The bounding rectangle of the feature within the application window
+     * in the window coordinate space.
      *
-     * <p>The bounds for features of type {@link #TYPE_FOLD fold} are guaranteed to be zero-high
-     * (for horizontal folds) or zero-wide (for vertical folds) and span the entire window.
-     *
-     * <p>The bounds for features of type {@link #TYPE_HINGE hinge} are guaranteed to span the
-     * entire window but, unlike folds, can have a non-zero area.
+     * @return bounds of display feature.
      */
     @NonNull
-    public Rect getBounds() {
-        return new Rect(mBounds);
-    }
-
-    /**
-     * Gets type of the physical display feature.
-     */
-    @Type
-    public int getType() {
-        return mType;
-    }
-
-    /**
-     * A fold in the flexible screen without a physical gap.
-     */
-    public static final int TYPE_FOLD = 1;
-
-    /**
-     * A physical separation with a hinge that allows two display panels to fold.
-     */
-    public static final int TYPE_HINGE = 2;
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({
-            TYPE_FOLD,
-            TYPE_HINGE,
-    })
-    @interface Type{}
-
-    private static String typeToString(@Type int type) {
-        switch (type) {
-            case TYPE_FOLD:
-                return "FOLD";
-            case TYPE_HINGE:
-                return "HINGE";
-            default:
-                return "Unknown feature type (" + type + ")";
-        }
-    }
-
-    @NonNull
-    @Override
-    public String toString() {
-        return "DisplayFeature{ bounds=" + mBounds + ", type=" + typeToString(mType) + " }";
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        DisplayFeature that = (DisplayFeature) o;
-
-        return mType == that.mType && mBounds.equals(that.mBounds);
-    }
-
-    @Override
-    public int hashCode() {
-        int result = mBounds.hashCode();
-        result = 31 * result + mType;
-        return result;
-    }
-
-    /**
-     * Builder for {@link DisplayFeature} objects.
-     */
-    public static final class Builder {
-        private Rect mBounds = new Rect();
-        @Type
-        private int mType = TYPE_FOLD;
-
-        /**
-         * Creates an initially empty builder.
-         */
-        public Builder() {
-        }
-
-        /**
-         * Sets the bounds for the {@link DisplayFeature} instance.
-         */
-        @NonNull
-        public Builder setBounds(@NonNull Rect bounds) {
-            mBounds = bounds;
-            return this;
-        }
-
-        /**
-         * Sets the type for the {@link DisplayFeature} instance.
-         */
-        @NonNull
-        public Builder setType(@Type int type) {
-            mType = type;
-            return this;
-        }
-
-        /**
-         * Creates a {@link DisplayFeature} instance with the specified fields.
-         * @return A DisplayFeature instance.
-         */
-        @NonNull
-        public DisplayFeature build() {
-            return new DisplayFeature(mBounds, mType);
-        }
-    }
-
-    /**
-     * Throws runtime exceptions if the bounds are invalid or incompatible with the supplied type.
-     */
-    private static void assertValidBounds(Rect bounds, @Type int type) {
-        if (bounds.height() == 0 && bounds.width() == 0) {
-            throw new IllegalArgumentException("Bounding rectangle must not be empty: " + bounds);
-        }
-
-        if (type == TYPE_FOLD) {
-            if (bounds.width() != 0 && bounds.height() != 0) {
-                throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
-                        + "or zero-high for features of type " + typeToString(type));
-            }
-
-            if ((bounds.width() != 0 && bounds.left != 0)
-                    || (bounds.height() != 0 && bounds.top != 0)) {
-                throw new IllegalArgumentException("Bounding rectangle must span the entire "
-                        + "window space for features of type " + typeToString(type));
-            }
-        } else if (type == TYPE_HINGE) {
-            if (bounds.left != 0 && bounds.top != 0) {
-                throw new IllegalArgumentException("Bounding rectangle must span the entire "
-                        + "window space for features of type " + typeToString(type));
-            }
-        }
-    }
+    Rect getBounds();
 }
diff --git a/window/window/src/main/java/androidx/window/ExtensionAdapter.java b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
index c2e5549..d3150e2 100644
--- a/window/window/src/main/java/androidx/window/ExtensionAdapter.java
+++ b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
@@ -23,6 +23,7 @@
 import androidx.annotation.Nullable;
 import androidx.window.extensions.ExtensionDeviceState;
 import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
 import androidx.window.extensions.ExtensionWindowLayoutInfo;
 
 import java.util.ArrayList;
@@ -37,9 +38,6 @@
     DeviceState translate(ExtensionDeviceState deviceState) {
         final int posture;
         switch (deviceState.getPosture()) {
-            case ExtensionDeviceState.POSTURE_CLOSED:
-                posture = DeviceState.POSTURE_CLOSED;
-                break;
             case ExtensionDeviceState.POSTURE_FLIPPED:
                 posture = DeviceState.POSTURE_FLIPPED;
                 break;
@@ -49,7 +47,6 @@
             case ExtensionDeviceState.POSTURE_OPENED:
                 posture = DeviceState.POSTURE_OPENED;
                 break;
-            case ExtensionDeviceState.POSTURE_UNKNOWN:
             default:
                 posture = DeviceState.POSTURE_UNKNOWN;
         }
@@ -79,39 +76,62 @@
     }
 
     @Nullable
-    DisplayFeature translate(Activity activity, ExtensionDisplayFeature feature) {
-        final Rect windowBounds = WindowBoundsHelper.getInstance()
-                .computeCurrentWindowBounds(activity);
-        if (!isValid(feature, windowBounds)) {
+    DisplayFeature translate(Activity activity, ExtensionDisplayFeature displayFeature) {
+        if (!(displayFeature instanceof ExtensionFoldingFeature)) {
             return null;
         }
-        int type = DisplayFeature.TYPE_FOLD;
-        switch (feature.getType()) {
-            case ExtensionDisplayFeature.TYPE_FOLD:
-                type = DisplayFeature.TYPE_FOLD;
-                break;
-            case ExtensionDisplayFeature.TYPE_HINGE:
-                type = DisplayFeature.TYPE_HINGE;
-                break;
-        }
-        return new DisplayFeature(feature.getBounds(), type);
+        ExtensionFoldingFeature feature = (ExtensionFoldingFeature) displayFeature;
+        final Rect windowBounds = WindowBoundsHelper.getInstance()
+                .computeCurrentWindowBounds(activity);
+        return translateFoldFeature(windowBounds, feature);
     }
 
-    boolean isValid(ExtensionDisplayFeature feature, Rect windowBounds) {
+    @Nullable
+    private static DisplayFeature translateFoldFeature(@NonNull Rect windowBounds,
+            @NonNull ExtensionFoldingFeature feature) {
+        if (!isValid(windowBounds, feature)) {
+            return null;
+        }
+        int type = FoldingFeature.TYPE_FOLD;
+        switch (feature.getType()) {
+            case ExtensionFoldingFeature.TYPE_FOLD:
+                type = FoldingFeature.TYPE_FOLD;
+                break;
+            case ExtensionFoldingFeature.TYPE_HINGE:
+                type = FoldingFeature.TYPE_HINGE;
+                break;
+        }
+        int state = FoldingFeature.STATE_FLAT;
+        switch (feature.getState()) {
+            case ExtensionFoldingFeature.STATE_FLAT:
+                state = FoldingFeature.STATE_FLAT;
+                break;
+            case ExtensionFoldingFeature.STATE_FLIPPED:
+                state = FoldingFeature.STATE_FLIPPED;
+                break;
+            case ExtensionFoldingFeature.STATE_HALF_OPENED:
+                state = FoldingFeature.STATE_HALF_OPENED;
+                break;
+        }
+        return new FoldingFeature(feature.getBounds(), type, state);
+    }
+
+    private static boolean isValid(Rect windowBounds, ExtensionFoldingFeature feature) {
         if (feature.getBounds().width() == 0 && feature.getBounds().height() == 0) {
             return false;
         }
-        if (feature.getType() == ExtensionDisplayFeature.TYPE_FOLD
+        if (feature.getType() == ExtensionFoldingFeature.TYPE_FOLD
                 && !feature.getBounds().isEmpty()) {
             return false;
         }
-        if (!hasMatchingDimension(feature.getBounds(), windowBounds)) {
+        if (feature.getType() != ExtensionFoldingFeature.TYPE_FOLD
+                && feature.getType() != ExtensionFoldingFeature.TYPE_HINGE) {
             return false;
         }
-        return true;
+        return hasMatchingDimension(feature.getBounds(), windowBounds);
     }
 
-    private boolean hasMatchingDimension(Rect lhs, Rect rhs) {
+    private static boolean hasMatchingDimension(Rect lhs, Rect rhs) {
         boolean matchesWidth = lhs.left == rhs.left && lhs.right == rhs.right;
         boolean matchesHeight = lhs.top == rhs.top && lhs.bottom == rhs.bottom;
         return matchesWidth || matchesHeight;
diff --git a/window/window/src/main/java/androidx/window/ExtensionCompat.java b/window/window/src/main/java/androidx/window/ExtensionCompat.java
index 8a09172..ca97067 100644
--- a/window/window/src/main/java/androidx/window/ExtensionCompat.java
+++ b/window/window/src/main/java/androidx/window/ExtensionCompat.java
@@ -25,8 +25,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.window.extensions.ExtensionDeviceState;
 import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
 import androidx.window.extensions.ExtensionInterface;
 import androidx.window.extensions.ExtensionProvider;
 import androidx.window.extensions.ExtensionWindowLayoutInfo;
@@ -137,23 +137,20 @@
                                 + rtUnregisterWindowLayoutChangeListener);
             }
 
-            // ExtensionDeviceState constructor
-            ExtensionDeviceState deviceState = new ExtensionDeviceState(
-                    ExtensionDeviceState.POSTURE_UNKNOWN);
+            // {@link ExtensionFoldingFeature} constructor
+            ExtensionFoldingFeature displayFoldingFeature =
+                    new ExtensionFoldingFeature(new Rect(0, 0, 100, 0),
+                            ExtensionFoldingFeature.TYPE_FOLD,
+                            ExtensionFoldingFeature.STATE_FLAT);
 
-            // deviceState.getPosture();
-            int tmpPosture = deviceState.getPosture();
+            // displayFoldFeature.getBounds()
+            Rect tmpRect = displayFoldingFeature.getBounds();
 
-            // ExtensionDisplayFeature constructor
-            ExtensionDisplayFeature displayFeature =
-                    new ExtensionDisplayFeature(new Rect(0, 0, 0, 0),
-                            ExtensionDisplayFeature.TYPE_FOLD);
+            // displayFoldFeature.getState()
+            int tmpState = displayFoldingFeature.getState();
 
-            // displayFeature.getBounds()
-            Rect tmpRect = displayFeature.getBounds();
-
-            // displayFeature.getType()
-            int tmpType = displayFeature.getType();
+            // displayFoldFeature.getType()
+            int tmpType = displayFoldingFeature.getType();
 
             // ExtensionWindowLayoutInfo constructor
             ExtensionWindowLayoutInfo windowLayoutInfo =
@@ -163,10 +160,10 @@
             List<ExtensionDisplayFeature> tmpDisplayFeatures =
                     windowLayoutInfo.getDisplayFeatures();
             return true;
-        } catch (Exception e) {
+        } catch (Throwable t) {
             if (DEBUG) {
                 Log.e(TAG, "Extension implementation doesn't conform to interface version "
-                        + getExtensionVersion() + ", error: " + e);
+                        + getExtensionVersion() + ", error: " + t);
             }
             return false;
         }
diff --git a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
index 44e0c47..6488073 100644
--- a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
+++ b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
@@ -77,8 +77,12 @@
 
     private static final String TAG = "WindowServer";
 
-    private ExtensionWindowBackend() {
-        // Empty
+    @VisibleForTesting
+    ExtensionWindowBackend(@Nullable ExtensionInterfaceCompat windowExtension) {
+        mWindowExtension = windowExtension;
+        if (mWindowExtension != null) {
+            mWindowExtension.setExtensionCallback(new ExtensionListenerImpl());
+        }
     }
 
     /**
@@ -89,25 +93,14 @@
         if (sInstance == null) {
             synchronized (sLock) {
                 if (sInstance == null) {
-                    sInstance = new ExtensionWindowBackend();
-                    sInstance.initExtension(context.getApplicationContext());
+                    ExtensionInterfaceCompat windowExtension = initAndVerifyExtension(context);
+                    sInstance = new ExtensionWindowBackend(windowExtension);
                 }
             }
         }
         return sInstance;
     }
 
-    /** Tries to initialize Extension, returns early if it's not available. */
-    @SuppressLint("SyntheticAccessor")
-    @GuardedBy("sLock")
-    private void initExtension(Context context) {
-        mWindowExtension = initAndVerifyExtension(context);
-        if (mWindowExtension == null) {
-            return;
-        }
-        mWindowExtension.setExtensionCallback(new ExtensionListenerImpl());
-    }
-
     @Override
     public void registerLayoutChangeCallback(@NonNull Activity activity,
             @NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
@@ -126,10 +119,12 @@
             WindowLayoutChangeCallbackWrapper callbackWrapper =
                     new WindowLayoutChangeCallbackWrapper(activity, executor, callback);
             mWindowLayoutChangeCallbacks.add(callbackWrapper);
+            // Read value before registering in case the extension updates synchronously.
+            // A synchronous update would result in two values emitted.
+            WindowLayoutInfo lastReportedValue = mLastReportedWindowLayouts.get(activity);
             if (!isActivityRegistered) {
                 mWindowExtension.onWindowLayoutChangeListenerAdded(activity);
             }
-            WindowLayoutInfo lastReportedValue = mLastReportedWindowLayouts.get(activity);
             if (lastReportedValue != null) {
                 callbackWrapper.accept(lastReportedValue);
             }
diff --git a/window/window/src/main/java/androidx/window/FoldingFeature.java b/window/window/src/main/java/androidx/window/FoldingFeature.java
new file mode 100644
index 0000000..34f2b68
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/FoldingFeature.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import android.graphics.Rect;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A feature that describes a fold in the flexible display
+ * or a hinge between two physical display panels.
+ */
+public class FoldingFeature implements DisplayFeature {
+
+    /**
+     * A fold in the flexible screen without a physical gap.
+     */
+    public static final int TYPE_FOLD = 1;
+
+    /**
+     * A physical separation with a hinge that allows two display panels to fold.
+     */
+    public static final int TYPE_HINGE = 2;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            TYPE_FOLD,
+            TYPE_HINGE,
+    })
+    @interface Type{}
+
+    /**
+     * The foldable device is completely open, the screen space that is presented to the user is
+     * flat. See the
+     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+     * section in the official documentation for visual samples and references.
+     */
+    public static final int STATE_FLAT = 1;
+
+    /**
+     * The foldable device's hinge is in an intermediate position between opened and closed state,
+     * there is a non-flat angle between parts of the flexible screen or between physical screen
+     * panels. See the
+     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+     * section in the official documentation for visual samples and references.
+     */
+    public static final int STATE_HALF_OPENED = 2;
+
+    /**
+     * The foldable device is flipped with the flexible screen parts or physical screens facing
+     * opposite directions. See the
+     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+     * section in the official documentation for visual samples and references.
+     */
+    public static final int STATE_FLIPPED = 3;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            STATE_HALF_OPENED,
+            STATE_FLAT,
+            STATE_FLIPPED,
+    })
+    @interface State {}
+
+    /**
+     * The bounding rectangle of the feature within the application window in the window
+     * coordinate space.
+     */
+    @NonNull
+    private final Rect mBounds;
+
+    /**
+     * The physical type of the feature.
+     */
+    @Type
+    private final int mType;
+
+    /**
+     * The state of the feature.
+     */
+    @State
+    private final int mState;
+
+    public FoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
+        validateFeatureBounds(bounds, type);
+        mBounds = new Rect(bounds);
+        mType = type;
+        mState = state;
+    }
+
+    @NonNull
+    @Override
+    public Rect getBounds() {
+        return new Rect(mBounds);
+    }
+
+    @Type
+    public int getType() {
+        return mType;
+    }
+
+    @State
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * Verifies the bounds of the folding feature.
+     */
+    private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+        if (bounds.width() == 0 && bounds.height() == 0) {
+            throw new IllegalArgumentException("Bounds must be non zero");
+        }
+        if (type == TYPE_FOLD) {
+            if (bounds.width() != 0 && bounds.height() != 0) {
+                throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
+                        + "or zero-high for features of type " + typeToString(type));
+            }
+
+            if ((bounds.width() != 0 && bounds.left != 0)
+                    || (bounds.height() != 0 && bounds.top != 0)) {
+                throw new IllegalArgumentException("Bounding rectangle must span the entire "
+                        + "window space for features of type " + typeToString(type));
+            }
+        } else if (type == TYPE_HINGE) {
+            if (bounds.left != 0 && bounds.top != 0) {
+                throw new IllegalArgumentException("Bounding rectangle must span the entire "
+                        + "window space for features of type " + typeToString(type));
+            }
+        }
+    }
+
+    @NonNull
+    private static String typeToString(int type) {
+        switch (type) {
+            case TYPE_FOLD:
+                return "FOLD";
+            case TYPE_HINGE:
+                return "HINGE";
+            default:
+                return "Unknown feature type (" + type + ")";
+        }
+    }
+
+    @NonNull
+    private static String stateToString(int state) {
+        switch (state) {
+            case STATE_FLAT:
+                return "FLAT";
+            case STATE_FLIPPED:
+                return "FLIPPED";
+            case STATE_HALF_OPENED:
+                return "HALF_OPENED";
+            default:
+                return "Unknown feature state (" + state + ")";
+        }
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return FoldingFeature.class.getSimpleName() + " { " + mBounds + ", type="
+                + typeToString(getType()) + ", state=" + stateToString(mState) + " }";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof FoldingFeature)) return false;
+        FoldingFeature that = (FoldingFeature) o;
+        return mType == that.mType
+            && mState == that.mState
+            && mBounds.equals(that.mBounds);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mBounds.hashCode();
+        result = 31 * result + mType;
+        result = 31 * result + mState;
+        return result;
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/SidecarAdapter.java b/window/window/src/main/java/androidx/window/SidecarAdapter.java
index 1a5ceb2..b7dcbdc 100644
--- a/window/window/src/main/java/androidx/window/SidecarAdapter.java
+++ b/window/window/src/main/java/androidx/window/SidecarAdapter.java
@@ -20,16 +20,20 @@
 import static androidx.window.DeviceState.POSTURE_UNKNOWN;
 import static androidx.window.ExtensionCompat.DEBUG;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.graphics.Rect;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.window.sidecar.SidecarDeviceState;
 import androidx.window.sidecar.SidecarDisplayFeature;
 import androidx.window.sidecar.SidecarWindowLayoutInfo;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -42,14 +46,17 @@
 
     @NonNull
     List<DisplayFeature> translate(SidecarWindowLayoutInfo sidecarWindowLayoutInfo,
-            Rect windowBounds) {
+            SidecarDeviceState deviceState, Rect windowBounds) {
         List<DisplayFeature> displayFeatures = new ArrayList<>();
-        if (sidecarWindowLayoutInfo.displayFeatures == null) {
+        List<SidecarDisplayFeature> sidecarDisplayFeatures =
+                getSidecarDisplayFeatures(sidecarWindowLayoutInfo);
+        if (sidecarDisplayFeatures == null) {
             return displayFeatures;
         }
 
-        for (SidecarDisplayFeature sidecarFeature : sidecarWindowLayoutInfo.displayFeatures) {
-            final DisplayFeature displayFeature = translate(sidecarFeature, windowBounds);
+        for (SidecarDisplayFeature sidecarFeature : sidecarDisplayFeatures) {
+            final DisplayFeature displayFeature = translate(sidecarFeature, deviceState,
+                    windowBounds);
             if (displayFeature != null) {
                 displayFeatures.add(displayFeature);
             }
@@ -57,32 +64,89 @@
         return displayFeatures;
     }
 
+    // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+    @SuppressLint("BanUncheckedReflection")
+    @SuppressWarnings("unchecked")
+    @VisibleForTesting
+    @Nullable
+    static List<SidecarDisplayFeature> getSidecarDisplayFeatures(SidecarWindowLayoutInfo info) {
+        try {
+            return info.displayFeatures;
+        } catch (NoSuchFieldError error) {
+            try {
+                Method methodGetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+                        "getDisplayFeatures");
+                return (List<SidecarDisplayFeature>) methodGetFeatures.invoke(info);
+            } catch (NoSuchMethodException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (IllegalAccessException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (InvocationTargetException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
+    // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+    @SuppressLint("BanUncheckedReflection")
+    @VisibleForTesting
+    static void setSidecarDisplayFeatures(SidecarWindowLayoutInfo info,
+            List<SidecarDisplayFeature> displayFeatures) {
+        try {
+            info.displayFeatures = displayFeatures;
+        } catch (NoSuchFieldError error) {
+            try {
+                Method methodSetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+                        "setDisplayFeatures", List.class);
+                methodSetFeatures.invoke(info, displayFeatures);
+            } catch (NoSuchMethodException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (IllegalAccessException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (InvocationTargetException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
     @NonNull
     WindowLayoutInfo translate(@NonNull Activity activity,
-            @Nullable SidecarWindowLayoutInfo extensionInfo) {
+            @Nullable SidecarWindowLayoutInfo extensionInfo, @NonNull SidecarDeviceState state) {
         if (extensionInfo == null) {
             return new WindowLayoutInfo(new ArrayList<>());
         }
 
+        SidecarDeviceState deviceState = new SidecarDeviceState();
+        int devicePosture = getSidecarDevicePosture(state);
+        setSidecarDevicePosture(deviceState, devicePosture);
+
         Rect windowBounds = WindowBoundsHelper.getInstance().computeCurrentWindowBounds(activity);
-        List<DisplayFeature> displayFeatures = translate(extensionInfo, windowBounds);
+        List<DisplayFeature> displayFeatures = translate(extensionInfo, deviceState, windowBounds);
         return new WindowLayoutInfo(displayFeatures);
     }
 
     @NonNull
-    DeviceState translate(@Nullable SidecarDeviceState sidecarDeviceState) {
-        if (sidecarDeviceState == null) {
-            return new DeviceState(POSTURE_UNKNOWN);
-        }
-
+    DeviceState translate(@NonNull SidecarDeviceState sidecarDeviceState) {
         int posture = postureFromSidecar(sidecarDeviceState);
         return new DeviceState(posture);
     }
 
-
     @DeviceState.Posture
     private static int postureFromSidecar(SidecarDeviceState sidecarDeviceState) {
-        int sidecarPosture = sidecarDeviceState.posture;
+        int sidecarPosture = getSidecarDevicePosture(sidecarDeviceState);
         if (sidecarPosture > POSTURE_MAX_KNOWN) {
             if (DEBUG) {
                 Log.d(TAG, "Unknown posture reported, WindowManager library should be updated");
@@ -92,12 +156,67 @@
         return sidecarPosture;
     }
 
+    // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+    @SuppressLint("BanUncheckedReflection")
+    @VisibleForTesting
+    static int getSidecarDevicePosture(SidecarDeviceState sidecarDeviceState) {
+        try {
+            return sidecarDeviceState.posture;
+        } catch (NoSuchFieldError error) {
+            try {
+                Method methodGetPosture = SidecarDeviceState.class.getMethod("getPosture");
+                return (int) methodGetPosture.invoke(sidecarDeviceState);
+            } catch (NoSuchMethodException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (IllegalAccessException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (InvocationTargetException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return SidecarDeviceState.POSTURE_UNKNOWN;
+    }
+
+    // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+    @SuppressLint("BanUncheckedReflection")
+    @VisibleForTesting
+    static void setSidecarDevicePosture(SidecarDeviceState sidecarDeviceState, int posture) {
+        try {
+            sidecarDeviceState.posture = posture;
+        } catch (NoSuchFieldError error) {
+            try {
+                Method methodSetPosture = SidecarDeviceState.class.getMethod("setPosture",
+                        int.class);
+                methodSetPosture.invoke(sidecarDeviceState, posture);
+            } catch (NoSuchMethodException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (IllegalAccessException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            } catch (InvocationTargetException e) {
+                if (DEBUG) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
     /**
      * Converts the display feature from extension. Can return {@code null} if there is an issue
      * with the value passed from extension.
      */
     @Nullable
-    private static DisplayFeature translate(SidecarDisplayFeature feature, Rect windowBounds) {
+    private static DisplayFeature translate(SidecarDisplayFeature feature,
+            SidecarDeviceState deviceState, Rect windowBounds) {
         Rect bounds = feature.getRect();
         if (bounds.width() == 0 && bounds.height() == 0) {
             if (DEBUG) {
@@ -131,6 +250,31 @@
             }
         }
 
-        return new DisplayFeature(feature.getRect(), feature.getType());
+        final int type;
+        if (feature.getType() == SidecarDisplayFeature.TYPE_HINGE) {
+            type = FoldingFeature.TYPE_HINGE;
+        } else {
+            type = FoldingFeature.TYPE_FOLD;
+        }
+
+        final int state;
+        final int devicePosture = getSidecarDevicePosture(deviceState);
+        switch (devicePosture) {
+            case SidecarDeviceState.POSTURE_CLOSED:
+            case SidecarDeviceState.POSTURE_UNKNOWN:
+                return null;
+            case SidecarDeviceState.POSTURE_FLIPPED:
+                state = FoldingFeature.STATE_FLIPPED;
+                break;
+            case SidecarDeviceState.POSTURE_HALF_OPENED:
+                state = FoldingFeature.STATE_HALF_OPENED;
+                break;
+            case SidecarDeviceState.POSTURE_OPENED:
+            default:
+                state = FoldingFeature.STATE_FLAT;
+                break;
+        }
+
+        return new FoldingFeature(feature.getRect(), type, state);
     }
 }
diff --git a/window/window/src/main/java/androidx/window/SidecarCompat.java b/window/window/src/main/java/androidx/window/SidecarCompat.java
index 7de2b77..295151b 100644
--- a/window/window/src/main/java/androidx/window/SidecarCompat.java
+++ b/window/window/src/main/java/androidx/window/SidecarCompat.java
@@ -32,7 +32,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.collection.SimpleArrayMap;
-import androidx.core.util.Consumer;
 import androidx.window.sidecar.SidecarDeviceState;
 import androidx.window.sidecar.SidecarDisplayFeature;
 import androidx.window.sidecar.SidecarInterface;
@@ -40,6 +39,7 @@
 import androidx.window.sidecar.SidecarWindowLayoutInfo;
 
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.List;
 
 /** Extension interface compatibility wrapper for v0.1 sidecar. */
@@ -53,7 +53,7 @@
             new SimpleArrayMap<>();
 
     private ExtensionCallbackInterface mExtensionCallback;
-    private SidecarAdapter mSidecarAdapter;
+    private final SidecarAdapter mSidecarAdapter;
 
     @VisibleForTesting
     final SidecarInterface mSidecar;
@@ -92,6 +92,17 @@
             @SuppressLint("SyntheticAccessor")
             public void onDeviceStateChanged(@NonNull SidecarDeviceState newDeviceState) {
                 extensionCallback.onDeviceStateChanged(mSidecarAdapter.translate(newDeviceState));
+
+                for (int i = 0; i < mWindowListenerRegisteredContexts.size(); i++) {
+                    Activity activity = mWindowListenerRegisteredContexts.valueAt(i);
+                    IBinder windowToken = getActivityWindowToken(activity);
+                    if (windowToken == null) {
+                        continue;
+                    }
+                    SidecarWindowLayoutInfo layoutInfo = mSidecar.getWindowLayoutInfo(windowToken);
+                    extensionCallback.onWindowLayoutChanged(activity,
+                            mSidecarAdapter.translate(activity, layoutInfo, newDeviceState));
+                }
             }
 
             @Override
@@ -106,7 +117,7 @@
                 }
 
                 extensionCallback.onWindowLayoutChanged(activity,
-                        mSidecarAdapter.translate(activity, newLayout));
+                        mSidecarAdapter.translate(activity, newLayout, mSidecar.getDeviceState()));
             }
         });
     }
@@ -117,7 +128,7 @@
         IBinder windowToken = getActivityWindowToken(activity);
 
         SidecarWindowLayoutInfo windowLayoutInfo = mSidecar.getWindowLayoutInfo(windowToken);
-        return mSidecarAdapter.translate(activity, windowLayoutInfo);
+        return mSidecarAdapter.translate(activity, windowLayoutInfo, mSidecar.getDeviceState());
     }
 
     @Override
@@ -127,7 +138,8 @@
         if (windowToken != null) {
             register(windowToken, activity);
         } else {
-            FirstAttachAdapter attachAdapter = new FirstAttachAdapter((token) -> {
+            FirstAttachAdapter attachAdapter = new FirstAttachAdapter(() -> {
+                IBinder token = getActivityWindowToken(activity);
                 register(token, activity);
             });
             activity.getWindow().getDecorView().addOnAttachStateChangeListener(attachAdapter);
@@ -159,6 +171,7 @@
         mSidecar.onDeviceStateListenersChanged(isEmpty);
     }
 
+    @SuppressLint("BanUncheckedReflection")
     @Override
     @SuppressWarnings("unused")
     public boolean validateExtensionInterface() {
@@ -215,7 +228,24 @@
             tmpDeviceState = new SidecarDeviceState();
 
             // deviceState.posture
-            tmpDeviceState.posture = SidecarDeviceState.POSTURE_OPENED;
+            // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+            try {
+                tmpDeviceState.posture = SidecarDeviceState.POSTURE_OPENED;
+            } catch (NoSuchFieldError error) {
+                if (DEBUG) {
+                    Log.w(TAG, "Sidecar implementation doesn't conform to primary interface "
+                            + "version, continue to check for the secondary one "
+                            + VERSION_0_1 + ", error: " + error);
+                }
+                Method methodSetPosture = SidecarDeviceState.class.getMethod("setPosture",
+                        int.class);
+                methodSetPosture.invoke(tmpDeviceState, SidecarDeviceState.POSTURE_OPENED);
+                Method methodGetPosture = SidecarDeviceState.class.getMethod("getPosture");
+                int posture = (int) methodGetPosture.invoke(tmpDeviceState);
+                if (posture != SidecarDeviceState.POSTURE_OPENED) {
+                    throw new Exception("Invalid device posture getter/setter");
+                }
+            }
 
             // SidecarDisplayFeature constructor
             SidecarDisplayFeature displayFeature = new SidecarDisplayFeature();
@@ -232,13 +262,36 @@
             SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
 
             // windowLayoutInfo.displayFeatures
-            final List<SidecarDisplayFeature> tmpDisplayFeatures = windowLayoutInfo.displayFeatures;
+            try {
+                final List<SidecarDisplayFeature> tmpDisplayFeatures =
+                        windowLayoutInfo.displayFeatures;
+                // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+            } catch (NoSuchFieldError error) {
+                if (DEBUG) {
+                    Log.w(TAG, "Sidecar implementation doesn't conform to primary interface "
+                            + "version, continue to check for the secondary one "
+                            + VERSION_0_1 + ", error: " + error);
+                }
+                List<SidecarDisplayFeature> featureList = new ArrayList<>();
+                featureList.add(displayFeature);
+                Method methodSetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+                        "setDisplayFeatures", List.class);
+                methodSetFeatures.invoke(windowLayoutInfo, featureList);
+                Method methodGetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+                        "getDisplayFeatures");
+                @SuppressWarnings("unchecked")
+                final List<SidecarDisplayFeature> resultDisplayFeatures =
+                        (List<SidecarDisplayFeature>) methodGetFeatures.invoke(windowLayoutInfo);
+                if (!featureList.equals(resultDisplayFeatures)) {
+                    throw new Exception("Invalid display feature getter/setter");
+                }
+            }
 
             return true;
-        } catch (Exception e) {
+        } catch (Throwable t) {
             if (DEBUG) {
-                Log.e(TAG, "Extension implementation doesn't conform to interface version "
-                        + VERSION_0_1 + ", error: " + e);
+                Log.e(TAG, "Sidecar implementation doesn't conform to interface version "
+                        + VERSION_0_1 + ", error: " + t);
             }
             return false;
         }
@@ -273,15 +326,15 @@
      */
     private static class FirstAttachAdapter implements View.OnAttachStateChangeListener {
 
-        private final Consumer<IBinder> mCallback;
+        private final Runnable mCallback;
 
-        FirstAttachAdapter(Consumer<IBinder> callback) {
+        FirstAttachAdapter(Runnable callback) {
             mCallback = callback;
         }
 
         @Override
         public void onViewAttachedToWindow(View view) {
-            mCallback.accept(view.getWindowToken());
+            mCallback.run();
             view.removeOnAttachStateChangeListener(this);
         }
 
diff --git a/window/window/src/main/java/androidx/window/WindowManager.java b/window/window/src/main/java/androidx/window/WindowManager.java
index 6d83906..56d2988 100644
--- a/window/window/src/main/java/androidx/window/WindowManager.java
+++ b/window/window/src/main/java/androidx/window/WindowManager.java
@@ -50,6 +50,7 @@
      * Gets an instance of the class initialized with and connected to the provided {@link Context}.
      * All methods of this class will return information that is associated with this visual
      * context.
+     *
      * @param context A visual context, such as an {@link Activity} or a {@link ContextWrapper}
      *                around one, to use for initialization.
      */
@@ -61,8 +62,10 @@
      * Gets an instance of the class initialized with and connected to the provided {@link Context}.
      * All methods of this class will return information that is associated with this visual
      * context.
-     * @param context A visual context, such as an {@link Activity} or a {@link ContextWrapper}
-     *                around one, to use for initialization.
+     *
+     * @param context       A visual context, such as an {@link Activity} or a
+     * {@link ContextWrapper}
+     *                      around one, to use for initialization.
      * @param windowBackend Backing server class that will provide information for this instance.
      *                      Pass a custom {@link WindowBackend} implementation for testing.
      */
@@ -80,6 +83,7 @@
     /**
      * Registers a callback for layout changes of the window of the current visual {@link Context}.
      * Must be called only after the it is attached to the window.
+     *
      * @see Activity#onAttachedToWindow()
      */
     public void registerLayoutChangeCallback(@NonNull Executor executor,
@@ -95,16 +99,20 @@
     }
 
     /**
+     * @deprecated {@link DeviceState} information has been merged into {@link WindowLayoutInfo}
      * Registers a callback for device state changes.
      */
+    @Deprecated
     public void registerDeviceStateChangeCallback(@NonNull Executor executor,
             @NonNull Consumer<DeviceState> callback) {
         mWindowBackend.registerDeviceStateChangeCallback(executor, callback);
     }
 
     /**
+     * @deprecated {@link DeviceState} information has been merged into {@link WindowLayoutInfo}
      * Unregisters a callback for device state changes.
      */
+    @Deprecated
     public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
         mWindowBackend.unregisterDeviceStateChangeCallback(callback);
     }
@@ -160,6 +168,7 @@
 
     /**
      * Unwraps the hierarchy of {@link ContextWrapper}-s until {@link Activity} is reached.
+     *
      * @return Base {@link Activity} context or {@code null} if not available.
      */
     @Nullable
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java b/window/window/src/test/java/androidx/window/ActivityTestUtil.java
similarity index 69%
rename from car/app/app/src/main/java/androidx/car/app/utils/Logger.java
rename to window/window/src/test/java/androidx/window/ActivityTestUtil.java
index a402482..2d6d5d7 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/Logger.java
+++ b/window/window/src/test/java/androidx/window/ActivityTestUtil.java
@@ -14,14 +14,18 @@
  * limitations under the License.
  */
 
-package androidx.car.app.utils;
+package androidx.window;
+
+import android.app.Activity;
+import android.os.IBinder;
 
 import androidx.annotation.NonNull;
 
-/**
- * Logger interface to allow the host to log while using the client library.
- */
-// TODO: Allow setting logging severity and including throwables
-public interface Logger {
-    void log(@NonNull String message);
+public class ActivityTestUtil {
+
+    private ActivityTestUtil() { }
+
+    static IBinder getActivityWindowToken(@NonNull Activity activity) {
+        return activity.getWindow().getAttributes().token;
+    }
 }
diff --git a/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
new file mode 100644
index 0000000..b79864f
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import com.google.common.collect.BoundType;
+import com.google.common.collect.Range;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ExtensionWindowBackend} that run on the JVM.
+ */
+@SuppressWarnings("deprecation") // TODO(b/173739071) Remove DeviceState
+public class ExtensionWindowBackendUnitTest {
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = mock(Context.class);
+        ExtensionWindowBackend.resetInstance();
+    }
+
+    @Test
+    public void testGetInstance() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        assertNotNull(backend);
+
+        // Verify that getInstance always returns the same value
+        ExtensionWindowBackend newBackend = ExtensionWindowBackend.getInstance(mContext);
+        assertEquals(backend, newBackend);
+    }
+
+    @Test
+    public void testRegisterDeviceStateChangeCallback_noExtension() {
+        // Verify method with extension
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        assumeTrue(backend.mWindowExtension == null);
+        SimpleConsumer<DeviceState> simpleConsumer = new SimpleConsumer<>();
+
+        backend.registerDeviceStateChangeCallback(directExecutor(), simpleConsumer);
+
+        DeviceState deviceState = simpleConsumer.lastValue();
+        assertNotNull(deviceState);
+        assertThat(deviceState.getPosture()).isIn(Range.range(
+                DeviceState.POSTURE_UNKNOWN, BoundType.CLOSED,
+                DeviceState.POSTURE_MAX_KNOWN, BoundType.CLOSED));
+        DeviceState initialLastReportedState = backend.mLastReportedDeviceState;
+
+        // Verify method without extension
+        backend.mWindowExtension = null;
+        SimpleConsumer<DeviceState> noExtensionConsumer = new SimpleConsumer<>();
+        backend.registerDeviceStateChangeCallback(directExecutor(), noExtensionConsumer);
+        deviceState = noExtensionConsumer.lastValue();
+        assertNotNull(deviceState);
+        assertEquals(DeviceState.POSTURE_UNKNOWN, deviceState.getPosture());
+        // Verify that last reported state does not change when using the getter
+        assertEquals(initialLastReportedState, backend.mLastReportedDeviceState);
+    }
+
+    @Test
+    public void testRegisterLayoutChangeCallback() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+        // Check registering the layout change callback
+        Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
+        Activity activity = mock(Activity.class);
+        backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
+
+        assertEquals(1, backend.mWindowLayoutChangeCallbacks.size());
+        verify(backend.mWindowExtension).onWindowLayoutChangeListenerAdded(activity);
+
+        // Check unregistering the layout change callback
+        backend.unregisterLayoutChangeCallback(consumer);
+
+        assertTrue(backend.mWindowLayoutChangeCallbacks.isEmpty());
+        verify(backend.mWindowExtension).onWindowLayoutChangeListenerRemoved(eq(activity));
+    }
+
+    @Test
+    public void testRegisterLayoutChangeCallback_synchronousExtension() {
+        WindowLayoutInfo expectedInfo = newTestWindowLayoutInfo();
+        ExtensionInterfaceCompat extensionInterfaceCompat =
+                new SynchronousExtensionInterface(expectedInfo,
+                newTestDeviceState());
+        ExtensionWindowBackend backend = new ExtensionWindowBackend(extensionInterfaceCompat);
+
+        // Check registering the layout change callback
+        Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
+        Activity activity = mock(Activity.class);
+        backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
+
+        // Check unregistering the layout change callback
+        verify(consumer).accept(expectedInfo);
+    }
+
+    @Test
+    public void testRegisterLayoutChangeCallback_callsExtensionOnce() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+        // Check registering the layout change callback
+        Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
+        Activity activity = mock(Activity.class);
+        backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
+        backend.registerLayoutChangeCallback(activity, Runnable::run,
+                mock(WindowLayoutInfoConsumer.class));
+
+        assertEquals(2, backend.mWindowLayoutChangeCallbacks.size());
+        verify(backend.mWindowExtension).onWindowLayoutChangeListenerAdded(activity);
+
+        // Check unregistering the layout change callback
+        backend.unregisterLayoutChangeCallback(consumer);
+
+        assertEquals(1, backend.mWindowLayoutChangeCallbacks.size());
+        verify(backend.mWindowExtension, times(0))
+                .onWindowLayoutChangeListenerRemoved(eq(activity));
+    }
+
+    @Test
+    public void testRegisterLayoutChangeCallback_clearListeners() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+        // Check registering the layout change callback
+        Consumer<WindowLayoutInfo> firstConsumer = mock(WindowLayoutInfoConsumer.class);
+        Consumer<WindowLayoutInfo> secondConsumer = mock(WindowLayoutInfoConsumer.class);
+        Activity activity = mock(Activity.class);
+        backend.registerLayoutChangeCallback(activity, Runnable::run, firstConsumer);
+        backend.registerLayoutChangeCallback(activity, Runnable::run, secondConsumer);
+
+        // Check unregistering the layout change callback
+        backend.unregisterLayoutChangeCallback(firstConsumer);
+        backend.unregisterLayoutChangeCallback(secondConsumer);
+
+        assertTrue(backend.mWindowLayoutChangeCallbacks.isEmpty());
+        verify(backend.mWindowExtension).onWindowLayoutChangeListenerRemoved(activity);
+    }
+
+    @Test
+    public void testRegisterDeviceChangeCallback() {
+        ExtensionInterfaceCompat mockInterface = mock(ExtensionInterfaceCompat.class);
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        backend.mWindowExtension = mockInterface;
+
+        // Check registering the device state change callback
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+
+        assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
+        verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(false));
+
+        // Check unregistering the device state change callback
+        backend.unregisterDeviceStateChangeCallback(consumer);
+
+        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
+        verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(true));
+    }
+
+    @Test
+    public void testDeviceChangeCallback() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+        // Check that callbacks from the extension are propagated correctly
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+
+        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+        DeviceState deviceState = newTestDeviceState();
+        ExtensionWindowBackend.ExtensionListenerImpl backendListener =
+                backend.new ExtensionListenerImpl();
+        backendListener.onDeviceStateChanged(deviceState);
+
+        verify(consumer, times(1)).accept(eq(deviceState));
+        assertEquals(deviceState, backend.mLastReportedDeviceState);
+
+        // Test that the same value wouldn't be reported again
+        backendListener.onDeviceStateChanged(deviceState);
+        verify(consumer, times(1)).accept(any());
+    }
+
+    @Test
+    public void testDeviceChangeChangeCallback_callsExtensionOnce() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+        // Check registering the layout change callback
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+        backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
+
+        assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
+        verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
+
+        // Check unregistering the layout change callback
+        backend.unregisterDeviceStateChangeCallback(consumer);
+
+        assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
+        verify(backend.mWindowExtension, times(0))
+                .onDeviceStateListenersChanged(true);
+    }
+
+    @Test
+    public void testDeviceChangeChangeCallback_clearListeners() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+        // Check registering the layout change callback
+        Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
+        Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
+        backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
+        backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
+
+        // Check unregistering the layout change callback
+        backend.unregisterDeviceStateChangeCallback(firstConsumer);
+        backend.unregisterDeviceStateChangeCallback(secondConsumer);
+
+        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
+        verify(backend.mWindowExtension).onDeviceStateListenersChanged(true);
+    }
+
+    @Test
+    public void testDeviceChangeCallback_relayLastEmittedValue() {
+        DeviceState expectedState = newTestDeviceState();
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+        backend.mLastReportedDeviceState = expectedState;
+
+        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+
+        verify(consumer).accept(expectedState);
+    }
+
+    @Test
+    public void testDeviceChangeCallback_clearLastEmittedValue() {
+        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+
+        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+        backend.unregisterDeviceStateChangeCallback(consumer);
+
+        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
+        assertNull(backend.mLastReportedDeviceState);
+    }
+
+    private static WindowLayoutInfo newTestWindowLayoutInfo() {
+        WindowLayoutInfo.Builder builder = new WindowLayoutInfo.Builder();
+        return builder.build();
+    }
+
+    private static DeviceState newTestDeviceState() {
+        DeviceState.Builder builder = new DeviceState.Builder();
+        builder.setPosture(DeviceState.POSTURE_OPENED);
+        return builder.build();
+    }
+
+    private interface DeviceStateConsumer extends Consumer<DeviceState> { }
+
+    private interface WindowLayoutInfoConsumer extends Consumer<WindowLayoutInfo> { }
+
+    private static class SimpleConsumer<T> implements Consumer<T> {
+        private final List<T> mValues;
+
+        SimpleConsumer() {
+            mValues = new ArrayList<>();
+        }
+
+        @Override
+        public void accept(T t) {
+            mValues.add(t);
+        }
+
+        T lastValue() {
+            return mValues.get(mValues.size() - 1);
+        }
+    }
+
+    private static class SynchronousExtensionInterface implements ExtensionInterfaceCompat {
+
+        private ExtensionCallbackInterface mInterface;
+        private final DeviceState mDeviceState;
+        private final WindowLayoutInfo mWindowLayoutInfo;
+
+        SynchronousExtensionInterface(WindowLayoutInfo windowLayoutInfo, DeviceState deviceState) {
+            mInterface = new ExtensionCallbackInterface() {
+                @Override
+                public void onDeviceStateChanged(@NonNull @NotNull DeviceState newDeviceState) {
+
+                }
+
+                @Override
+                public void onWindowLayoutChanged(@NonNull @NotNull Activity activity,
+                        @NonNull @NotNull WindowLayoutInfo newLayout) {
+
+                }
+            };
+            mWindowLayoutInfo = windowLayoutInfo;
+            mDeviceState = deviceState;
+        }
+
+        @Override
+        public boolean validateExtensionInterface() {
+            return true;
+        }
+
+        @Override
+        public void setExtensionCallback(
+                @NonNull @NotNull ExtensionCallbackInterface extensionCallback) {
+            mInterface = extensionCallback;
+        }
+
+        @Override
+        public void onWindowLayoutChangeListenerAdded(@NonNull @NotNull Activity activity) {
+            mInterface.onWindowLayoutChanged(activity, mWindowLayoutInfo);
+        }
+
+        @Override
+        public void onWindowLayoutChangeListenerRemoved(@NonNull @NotNull Activity activity) {
+
+        }
+
+        @Override
+        public void onDeviceStateListenersChanged(boolean isEmpty) {
+            mInterface.onDeviceStateChanged(mDeviceState);
+        }
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
new file mode 100644
index 0000000..a7b9da2
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import static androidx.window.ActivityTestUtil.getActivityWindowToken;
+import static androidx.window.TestBoundsUtil.invalidFoldBounds;
+import static androidx.window.TestBoundsUtil.invalidHingeBounds;
+import static androidx.window.TestBoundsUtil.validFoldBound;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.window.sidecar.SidecarDeviceState;
+import androidx.window.sidecar.SidecarDisplayFeature;
+import androidx.window.sidecar.SidecarInterface;
+import androidx.window.sidecar.SidecarWindowLayoutInfo;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link SidecarCompat} that run on the JVM.
+ */
+public final class SidecarCompatUnitTest {
+
+    private static final Rect WINDOW_BOUNDS = new Rect(1, 1, 50, 100);
+
+    private Activity mActivity;
+    private SidecarCompat mSidecarCompat;
+
+    @Before
+    public void setUp() {
+        TestWindowBoundsHelper mWindowBoundsHelper = new TestWindowBoundsHelper();
+        mWindowBoundsHelper.setCurrentBounds(WINDOW_BOUNDS);
+        WindowBoundsHelper.setForTesting(mWindowBoundsHelper);
+
+        mActivity = mock(Activity.class);
+        Window window = spy(new TestWindow(mActivity));
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        doReturn(params).when(window).getAttributes();
+        when(mActivity.getWindow()).thenReturn(window);
+
+        SidecarInterface mockSidecarInterface = mock(SidecarInterface.class);
+        when(mockSidecarInterface.getDeviceState()).thenReturn(
+                newDeviceState(DeviceState.POSTURE_FLIPPED));
+        when(mockSidecarInterface.getWindowLayoutInfo(any())).thenReturn(
+                newWindowLayoutInfo(new ArrayList<>()));
+        mSidecarCompat = new SidecarCompat(mockSidecarInterface, new SidecarAdapter());
+    }
+
+    @After
+    public void tearDown() {
+        WindowBoundsHelper.setForTesting(null);
+    }
+
+    @Test
+    public void testGetDeviceState() {
+        FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+        SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
+        ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
+                ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+        compat.setExtensionCallback(mockCallback);
+        compat.onDeviceStateListenersChanged(false);
+        SidecarDeviceState deviceState = newDeviceState(SidecarDeviceState.POSTURE_OPENED);
+
+        fakeSidecarImp.triggerDeviceState(deviceState);
+
+        verify(mockCallback).onDeviceStateChanged(any());
+    }
+
+    @Test
+    public void testGetWindowLayout_featureWithEmptyBounds() {
+        // Add a feature with an empty bounds to the reported list
+        SidecarWindowLayoutInfo originalWindowLayoutInfo =
+                mSidecarCompat.mSidecar.getWindowLayoutInfo(getActivityWindowToken(mActivity));
+        List<SidecarDisplayFeature> sidecarDisplayFeatures =
+                originalWindowLayoutInfo.displayFeatures;
+        SidecarDisplayFeature newFeature = new SidecarDisplayFeature();
+        newFeature.setRect(new Rect());
+        sidecarDisplayFeatures.add(newFeature);
+
+        // Verify that this feature is skipped.
+        WindowLayoutInfo windowLayoutInfo = mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+        assertEquals(sidecarDisplayFeatures.size() - 1,
+                windowLayoutInfo.getDisplayFeatures().size());
+    }
+
+    @Test
+    public void testGetWindowLayout_foldWithNonZeroArea() {
+        SidecarWindowLayoutInfo originalWindowLayoutInfo =
+                mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
+        List<SidecarDisplayFeature> sidecarDisplayFeatures =
+                originalWindowLayoutInfo.displayFeatures;
+        // Horizontal fold.
+        sidecarDisplayFeatures.add(
+                newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width(), 2),
+                        SidecarDisplayFeature.TYPE_FOLD));
+        // Vertical fold.
+        sidecarDisplayFeatures.add(
+                newDisplayFeature(new Rect(1, 0, 2, WINDOW_BOUNDS.height()),
+                        SidecarDisplayFeature.TYPE_FOLD));
+
+        // Verify that these features are skipped.
+        WindowLayoutInfo windowLayoutInfo =
+                mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+        assertEquals(sidecarDisplayFeatures.size() - 2,
+                windowLayoutInfo.getDisplayFeatures().size());
+    }
+
+    @Test
+    public void testGetWindowLayout_hingeNotSpanningEntireWindow() {
+        SidecarWindowLayoutInfo originalWindowLayoutInfo =
+                mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
+        List<SidecarDisplayFeature> sidecarDisplayFeatures =
+                originalWindowLayoutInfo.displayFeatures;
+        // Horizontal hinge.
+        sidecarDisplayFeatures.add(
+                newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
+                        SidecarDisplayFeature.TYPE_FOLD));
+        // Vertical hinge.
+        sidecarDisplayFeatures.add(
+                newDisplayFeature(new Rect(1, 0, 2, WINDOW_BOUNDS.height() - 1),
+                        SidecarDisplayFeature.TYPE_FOLD));
+
+        // Verify that these features are skipped.
+        WindowLayoutInfo windowLayoutInfo =
+                mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+        assertEquals(sidecarDisplayFeatures.size() - 2,
+                windowLayoutInfo.getDisplayFeatures().size());
+    }
+
+    @Test
+    public void testGetWindowLayout_foldNotSpanningEntireWindow() {
+        SidecarWindowLayoutInfo originalWindowLayoutInfo =
+                mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
+        List<SidecarDisplayFeature> sidecarDisplayFeatures =
+                originalWindowLayoutInfo.displayFeatures;
+        // Horizontal fold.
+        sidecarDisplayFeatures.add(
+                newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
+                        SidecarDisplayFeature.TYPE_FOLD));
+        // Vertical fold.
+        sidecarDisplayFeatures.add(
+                newDisplayFeature(new Rect(1, 0, 2, WINDOW_BOUNDS.height() - 1),
+                        SidecarDisplayFeature.TYPE_FOLD));
+
+        // Verify that these features are skipped.
+        WindowLayoutInfo windowLayoutInfo =
+                mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+        assertEquals(sidecarDisplayFeatures.size() - 2,
+                windowLayoutInfo.getDisplayFeatures().size());
+    }
+
+    @Test
+    public void testOnWindowLayoutChangeListenerAdded() {
+        IBinder expectedToken = mock(IBinder.class);
+        mActivity.getWindow().getAttributes().token = expectedToken;
+        mSidecarCompat.onWindowLayoutChangeListenerAdded(mActivity);
+        verify(mSidecarCompat.mSidecar).onWindowLayoutChangeListenerAdded(eq(expectedToken));
+    }
+
+    @Test
+    public void testOnWindowLayoutChangeListenerAdded_emitInitialValueDelayed() {
+        SidecarWindowLayoutInfo layoutInfo = new SidecarWindowLayoutInfo();
+        WindowLayoutInfo expectedLayoutInfo = new WindowLayoutInfo(new ArrayList<>());
+        ExtensionInterfaceCompat.ExtensionCallbackInterface listener =
+                mock(ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+        mSidecarCompat.setExtensionCallback(listener);
+        when(mSidecarCompat.mSidecar.getWindowLayoutInfo(any())).thenReturn(layoutInfo);
+        View fakeView = mock(View.class);
+        Window mockWindow = mock(Window.class);
+        when(mockWindow.getAttributes()).thenReturn(new WindowManager.LayoutParams());
+        doAnswer(invocation -> {
+            View.OnAttachStateChangeListener stateChangeListener = invocation.getArgument(0);
+            mockWindow.getAttributes().token = mock(IBinder.class);
+            stateChangeListener.onViewAttachedToWindow(fakeView);
+            return null;
+        }).when(fakeView).addOnAttachStateChangeListener(any());
+        when(mockWindow.getDecorView()).thenReturn(fakeView);
+        when(mActivity.getWindow()).thenReturn(mockWindow);
+
+        mSidecarCompat.onWindowLayoutChangeListenerAdded(mActivity);
+
+        verify(listener).onWindowLayoutChanged(mActivity, expectedLayoutInfo);
+        verify(mSidecarCompat.mSidecar).onWindowLayoutChangeListenerAdded(
+                getActivityWindowToken(mActivity));
+        verify(fakeView).addOnAttachStateChangeListener(any());
+    }
+
+    @Test
+    public void testOnWindowLayoutChangeListenerRemoved() {
+        IBinder windowToken = getActivityWindowToken(mActivity);
+        mSidecarCompat.onWindowLayoutChangeListenerRemoved(mActivity);
+        verify(mSidecarCompat.mSidecar).onWindowLayoutChangeListenerRemoved(eq(windowToken));
+    }
+
+    @Test
+    public void testOnDeviceStateListenersChanged() {
+        mSidecarCompat.onDeviceStateListenersChanged(true);
+        verify(mSidecarCompat.mSidecar).onDeviceStateListenersChanged(eq(true));
+    }
+
+    @Test
+    public void testOnDeviceStateListenersAdded_emitInitialValue() {
+        SidecarDeviceState deviceState = new SidecarDeviceState();
+        DeviceState expectedDeviceState = new DeviceState(DeviceState.POSTURE_UNKNOWN);
+        ExtensionInterfaceCompat.ExtensionCallbackInterface listener =
+                mock(ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+        mSidecarCompat.setExtensionCallback(listener);
+        when(mSidecarCompat.mSidecar.getDeviceState()).thenReturn(deviceState);
+
+        mSidecarCompat.onDeviceStateListenersChanged(false);
+
+        verify(listener).onDeviceStateChanged(expectedDeviceState);
+    }
+
+    private static SidecarDisplayFeature newDisplayFeature(Rect rect, int type) {
+        SidecarDisplayFeature feature = new SidecarDisplayFeature();
+        feature.setRect(rect);
+        feature.setType(type);
+        return feature;
+    }
+
+    private static SidecarWindowLayoutInfo newWindowLayoutInfo(
+            List<SidecarDisplayFeature> features) {
+        SidecarWindowLayoutInfo info = new SidecarWindowLayoutInfo();
+        info.displayFeatures = new ArrayList<>();
+        info.displayFeatures.addAll(features);
+        return info;
+    }
+
+    private static SidecarDeviceState newDeviceState(int posture) {
+        SidecarDeviceState state = new SidecarDeviceState();
+        state.posture = posture;
+        return state;
+    }
+
+    private static final class FakeExtensionImp implements SidecarInterface {
+
+        private SidecarInterface.SidecarCallback mCallback;
+        private List<IBinder> mTokens = new ArrayList<>();
+
+        FakeExtensionImp() {
+            mCallback = new SidecarInterface.SidecarCallback() {
+                @Override
+                public void onDeviceStateChanged(@NonNull SidecarDeviceState newDeviceState) {
+
+                }
+
+                @Override
+                public void onWindowLayoutChanged(@NonNull IBinder windowToken,
+                        @NonNull SidecarWindowLayoutInfo newLayout) {
+
+                }
+            };
+        }
+
+        @Override
+        public void setSidecarCallback(@NonNull SidecarCallback callback) {
+
+        }
+
+        @NonNull
+        @Override
+        public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
+            return null;
+        }
+
+        @Override
+        public void onWindowLayoutChangeListenerAdded(@NonNull IBinder windowToken) {
+
+        }
+
+        @Override
+        public void onWindowLayoutChangeListenerRemoved(@NonNull IBinder windowToken) {
+
+        }
+
+        @NonNull
+        @Override
+        public SidecarDeviceState getDeviceState() {
+            SidecarDeviceState state = new SidecarDeviceState();
+            return state;
+        }
+
+        @Override
+        public void onDeviceStateListenersChanged(boolean isEmpty) {
+
+        }
+
+        void triggerMalformedSignal() {
+            triggerSignal(malformedWindowLayoutInfo());
+        }
+
+        void triggerGoodSignal() {
+            triggerSignal(validWindowLayoutInfo());
+        }
+
+        void triggerSignal(SidecarWindowLayoutInfo info) {
+            for (IBinder token: mTokens) {
+                triggerSignal(token, info);
+            }
+        }
+
+        void triggerSignal(IBinder token, SidecarWindowLayoutInfo info) {
+            mCallback.onWindowLayoutChanged(token, info);
+        }
+
+        public void triggerDeviceState(SidecarDeviceState state) {
+            mCallback.onDeviceStateChanged(state);
+
+        }
+
+        private SidecarWindowLayoutInfo malformedWindowLayoutInfo() {
+            List<SidecarDisplayFeature> malformedFeatures = new ArrayList<>();
+
+            for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
+                malformedFeatures.add(newDisplayFeature(malformedBound,
+                        SidecarDisplayFeature.TYPE_FOLD));
+            }
+
+            for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+                malformedFeatures.add(newDisplayFeature(malformedBound,
+                        SidecarDisplayFeature.TYPE_HINGE));
+            }
+
+            return newWindowLayoutInfo(malformedFeatures);
+        }
+
+        private SidecarWindowLayoutInfo validWindowLayoutInfo() {
+            List<SidecarDisplayFeature> goodFeatures = new ArrayList<>();
+
+            goodFeatures.add(newDisplayFeature(validFoldBound(WINDOW_BOUNDS),
+                    SidecarDisplayFeature.TYPE_FOLD));
+
+            return newWindowLayoutInfo(goodFeatures);
+        }
+    }
+}
diff --git a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java b/window/window/src/test/java/androidx/window/TestBoundsUtil.java
similarity index 91%
rename from window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
rename to window/window/src/test/java/androidx/window/TestBoundsUtil.java
index 18447f5..4988e40 100644
--- a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
+++ b/window/window/src/test/java/androidx/window/TestBoundsUtil.java
@@ -24,7 +24,7 @@
 /**
  * A utility class to provide bounds for a display feature
  */
-class TestBoundUtil {
+class TestBoundsUtil {
 
     public static Rect validFoldBound(Rect windowBounds) {
         return new Rect(windowBounds.left, windowBounds.top, windowBounds.right, 0);
@@ -38,7 +38,7 @@
         return new Rect(windowBounds.left, windowBounds.top, windowBounds.right / 2, 2);
     }
 
-    public static Rect invalidBoundShortHeightHeight(Rect windowBounds) {
+    public static Rect invalidBoundShortHeight(Rect windowBounds) {
         return new Rect(windowBounds.left, windowBounds.top, 2, windowBounds.bottom / 2);
     }
 
@@ -47,7 +47,7 @@
 
         badBounds.add(invalidZeroBound());
         badBounds.add(invalidBoundShortWidth(windowBounds));
-        badBounds.add(invalidBoundShortHeightHeight(windowBounds));
+        badBounds.add(invalidBoundShortHeight(windowBounds));
 
         return badBounds;
     }
diff --git a/window/window/src/test/java/androidx/window/TestWindow.java b/window/window/src/test/java/androidx/window/TestWindow.java
new file mode 100644
index 0000000..8e3591f
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/TestWindow.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.InputQueue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class TestWindow extends Window {
+
+    private View mDecorView;
+
+    public TestWindow(Context context) {
+        this(context, mock(View.class));
+    }
+
+    public TestWindow(Context context, View decorView) {
+        super(context);
+        mDecorView = decorView;
+    }
+
+    @Override
+    public void takeSurface(SurfaceHolder.Callback2 callback) {
+
+    }
+
+    @Override
+    public void takeInputQueue(InputQueue.Callback callback) {
+
+    }
+
+    @Override
+    public boolean isFloating() {
+        return false;
+    }
+
+    @Override
+    public void setContentView(int layoutResID) {
+
+    }
+
+    @Override
+    public void setContentView(View view) {
+
+    }
+
+    @Override
+    public void setContentView(View view, ViewGroup.LayoutParams params) {
+
+    }
+
+    @Override
+    public void addContentView(View view, ViewGroup.LayoutParams params) {
+
+    }
+
+    @Nullable
+    @Override
+    public View getCurrentFocus() {
+        return null;
+    }
+
+    @NonNull
+    @Override
+    public LayoutInflater getLayoutInflater() {
+        return null;
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+
+    }
+
+    @Override
+    public void setTitleColor(int textColor) {
+
+    }
+
+    @Override
+    public void openPanel(int featureId, KeyEvent event) {
+
+    }
+
+    @Override
+    public void closePanel(int featureId) {
+
+    }
+
+    @Override
+    public void togglePanel(int featureId, KeyEvent event) {
+
+    }
+
+    @Override
+    public void invalidatePanelMenu(int featureId) {
+
+    }
+
+    @Override
+    public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) {
+        return false;
+    }
+
+    @Override
+    public boolean performPanelIdentifierAction(int featureId, int id, int flags) {
+        return false;
+    }
+
+    @Override
+    public void closeAllPanels() {
+
+    }
+
+    @Override
+    public boolean performContextMenuIdentifierAction(int id, int flags) {
+        return false;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+
+    }
+
+    @Override
+    public void setBackgroundDrawable(Drawable drawable) {
+
+    }
+
+    @Override
+    public void setFeatureDrawableResource(int featureId, int resId) {
+
+    }
+
+    @Override
+    public void setFeatureDrawableUri(int featureId, Uri uri) {
+
+    }
+
+    @Override
+    public void setFeatureDrawable(int featureId, Drawable drawable) {
+
+    }
+
+    @Override
+    public void setFeatureDrawableAlpha(int featureId, int alpha) {
+
+    }
+
+    @Override
+    public void setFeatureInt(int featureId, int value) {
+
+    }
+
+    @Override
+    public void takeKeyEvents(boolean get) {
+
+    }
+
+    @Override
+    public boolean superDispatchKeyEvent(KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean superDispatchTouchEvent(MotionEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean superDispatchTrackballEvent(MotionEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean superDispatchGenericMotionEvent(MotionEvent event) {
+        return false;
+    }
+
+    @NonNull
+    @Override
+    public View getDecorView() {
+        return mDecorView;
+    }
+
+    @Override
+    public View peekDecorView() {
+        return null;
+    }
+
+    @Override
+    public Bundle saveHierarchyState() {
+        return null;
+    }
+
+    @Override
+    public void restoreHierarchyState(Bundle savedInstanceState) {
+
+    }
+
+    @Override
+    protected void onActive() {
+
+    }
+
+    @Override
+    public void setChildDrawable(int featureId, Drawable drawable) {
+
+    }
+
+    @Override
+    public void setChildInt(int featureId, int value) {
+
+    }
+
+    @Override
+    public boolean isShortcutKey(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public void setVolumeControlStream(int streamType) {
+
+    }
+
+    @Override
+    public int getVolumeControlStream() {
+        return 0;
+    }
+
+    @Override
+    public int getStatusBarColor() {
+        return 0;
+    }
+
+    @Override
+    public void setStatusBarColor(int color) {
+
+    }
+
+    @Override
+    public int getNavigationBarColor() {
+        return 0;
+    }
+
+    @Override
+    public void setNavigationBarColor(int color) {
+
+    }
+
+    @Override
+    public void setDecorCaptionShade(int decorCaptionShade) {
+
+    }
+
+    @Override
+    public void setResizingCaptionDrawable(Drawable drawable) {
+
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/TestWindowBoundsHelper.java b/window/window/src/test/java/androidx/window/TestWindowBoundsHelper.java
new file mode 100644
index 0000000..15a069e
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/TestWindowBoundsHelper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import android.app.Activity;
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.HashMap;
+
+/**
+ * Subclass of {@link WindowBoundsHelper} used to override the results for testing.
+ *
+ * @see WindowBoundsHelper
+ * @see WindowBoundsHelper#setForTesting(WindowBoundsHelper)
+ */
+class TestWindowBoundsHelper extends WindowBoundsHelper {
+    private Rect mGlobalOverriddenBounds;
+    private final HashMap<Activity, Rect> mOverriddenBounds = new HashMap<>();
+    private final HashMap<Activity, Rect> mOverriddenMaximumBounds = new HashMap<>();
+
+    /**
+     * Overrides the bounds returned from this helper for the given context. Passing null {@code
+     * bounds} has the effect of clearing the bounds override.
+     * <p>
+     * Note: A global override set as a result of {@link #setCurrentBounds(Rect)} takes precedence
+     * over the value set with this method.
+     */
+    void setCurrentBoundsForActivity(@NonNull Activity activity, @Nullable Rect bounds) {
+        mOverriddenBounds.put(activity, bounds);
+    }
+
+    /**
+     * Overrides the max bounds returned from this helper for the given context. Passing {@code
+     * null} {@code bounds} has the effect of clearing the bounds override.
+     */
+    void setMaximumBoundsForActivity(@NonNull Activity activity, @Nullable Rect bounds) {
+        mOverriddenMaximumBounds.put(activity, bounds);
+    }
+
+    /**
+     * Overrides the bounds returned from this helper for all supplied contexts. Passing null
+     * {@code bounds} has the effect of clearing the global override.
+     */
+    void setCurrentBounds(@Nullable Rect bounds) {
+        mGlobalOverriddenBounds = bounds;
+    }
+
+    @Override
+    @NonNull
+    Rect computeCurrentWindowBounds(Activity activity) {
+        if (mGlobalOverriddenBounds != null) {
+            return mGlobalOverriddenBounds;
+        }
+
+        Rect bounds = mOverriddenBounds.get(activity);
+        if (bounds != null) {
+            return bounds;
+        }
+
+        return super.computeCurrentWindowBounds(activity);
+    }
+
+    @NonNull
+    @Override
+    Rect computeMaximumWindowBounds(Activity activity) {
+        Rect bounds = mOverriddenMaximumBounds.get(activity);
+        if (bounds != null) {
+            return bounds;
+        }
+
+        return super.computeMaximumWindowBounds(activity);
+    }
+
+    /**
+     * Clears any overrides set with {@link #setCurrentBounds(Rect)} or
+     * {@link #setCurrentBoundsForActivity(Activity, Rect)}.
+     */
+    void reset() {
+        mGlobalOverriddenBounds = null;
+        mOverriddenBounds.clear();
+        mOverriddenMaximumBounds.clear();
+    }
+}