Merge "Clean up coroutines in repeated tests like benchmarks" into androidx-master-dev
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index e683bb6..1159550 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -27,6 +27,7 @@
     api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
     api(projectOrArtifact(":savedstate:savedstate"))
     api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+    implementation("androidx.tracing:tracing:1.0.0")
 
     androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(KOTLIN_STDLIB)
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..8d44a2b 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) {
+    public InputConnection onCreateInputConnection(@NonNull 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/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/AppSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
index 91af24c..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
@@ -284,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.
@@ -307,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
@@ -363,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.
@@ -387,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");
@@ -698,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
index 3830b9d..389c3ee 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
@@ -39,7 +39,6 @@
     public void testDeepEquals_self() {
         Bundle one = new Bundle();
         one.putString("a", "a");
-        assertThat(one).isEqualTo(one);
         assertThat(BundleUtil.deepEquals(one, one)).isTrue();
     }
 
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 ffc73249..0a46469 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -192,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/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/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 cad539b..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
@@ -279,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);
         }
@@ -327,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");
     }
@@ -351,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);
 
@@ -417,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()
@@ -437,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.
@@ -459,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()
@@ -479,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()
@@ -508,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()
@@ -534,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.
@@ -557,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(
@@ -567,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(
@@ -577,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();
@@ -587,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();
@@ -597,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");
     }
@@ -609,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();
@@ -624,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/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 2ec2d82..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,17 +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) {
+            for (int i = 0; i < schemas.size(); i++) {
                 SchemaTypeConfigProto schemaTypeProto =
-                        SchemaToProtoConverter.toSchemaTypeConfigProto(schema);
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(schemas.get(i));
                 newSchemaBuilder.addTypes(schemaTypeProto);
             }
 
@@ -281,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);
@@ -306,11 +310,11 @@
      *
      * <p>This method belongs to query group.
      *
-     * @param databaseName  The name of the database where this schema lives.
+     * @param databaseName The name of the database where this schema lives.
      * @throws AppSearchException on IcingSearchEngine error.
      */
     @NonNull
-    public Set<AppSearchSchema> getSchema(@NonNull String databaseName) throws AppSearchException {
+    public List<AppSearchSchema> getSchema(@NonNull String databaseName) throws AppSearchException {
         SchemaProto fullSchema;
         mReadWriteLock.readLock().lock();
         try {
@@ -319,7 +323,7 @@
             mReadWriteLock.readLock().unlock();
         }
 
-        Set<AppSearchSchema> result = new ArraySet<>();
+        List<AppSearchSchema> result = new ArrayList<>();
         for (int i = 0; i < fullSchema.getTypesCount(); i++) {
             String typeDatabase = getDatabaseName(fullSchema.getTypes(i).getSchemaType());
             if (!databaseName.equals(typeDatabase)) {
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 7d87f11..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;
@@ -50,8 +51,13 @@
  * 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;
 
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 12b25fa..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
@@ -31,10 +31,13 @@
 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;
@@ -67,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);
@@ -81,7 +86,8 @@
     public ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema() {
         return execute(() -> {
             try {
-                return AppSearchResult.newSuccessfulResult(mAppSearchImpl.getSchema(mDatabaseName));
+                List<AppSearchSchema> schemas = mAppSearchImpl.getSchema(mDatabaseName);
+                return AppSearchResult.newSuccessfulResult(new ArraySet<>(schemas));
             } catch (Throwable t) {
                 return throwableToFailedResult(t);
             }
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/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/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
index 5cf2081..52792d1 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
@@ -16,11 +16,8 @@
 
 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 TrivialStartupActivity : AppCompatActivity() {
@@ -30,18 +27,5 @@
 
         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 fa8493f..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,46 +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"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+<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"
-    android:minHeight="64dp"
-    android:padding="8dp">
-
-    <androidx.cardview.widget.CardView
-        android:id="@+id/card"
+    android:layout_margin="8dp">
+    <androidx.appcompat.widget.LinearLayoutCompat
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:padding="8dp">
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
 
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/content"
+            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.appcompat.widget.AppCompatCheckBox
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="end"
-                android:layout_marginEnd="16dp"
-                android:layout_marginRight="16dp"
-                android:layout_marginTop="8dp"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toTopOf="parent" />
-
-            <androidx.appcompat.widget.AppCompatTextView
-                android:id="@+id/content"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="16dp"
-                android:layout_marginStart="16dp"
-                android:layout_marginTop="8dp"
-                android:padding="8dp"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent"
-                tools:text="Sample text" />
-        </androidx.constraintlayout.widget.ConstraintLayout>
-    </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/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 4a88391..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,8 +18,10 @@
 
 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
@@ -93,12 +95,15 @@
  * This function is a building block for public testing APIs
  */
 fun macrobenchmark(
-    benchmarkName: String,
+    uniqueName: String,
+    className: String,
+    testName: String,
     config: MacrobenchmarkConfig,
     launchWithClearTask: Boolean,
     setupBlock: MacrobenchmarkScope.(Boolean) -> Unit,
     measureBlock: MacrobenchmarkScope.() -> Unit
 ) = withPermissiveSeLinuxPolicy {
+    val startTime = System.nanoTime()
     val scope = MacrobenchmarkScope(config.packageName, launchWithClearTask)
 
     // always kill the process at beginning of test
@@ -117,7 +122,7 @@
             it.configure(config)
         }
         var isFirstRun = true
-        val results = List(config.iterations) { iteration ->
+        val metricResults = List(config.iterations) { iteration ->
             setupBlock(scope, isFirstRun)
             isFirstRun = false
             try {
@@ -130,7 +135,7 @@
                 config.metrics.forEach {
                     it.stop()
                 }
-                perfettoCollector.stop(benchmarkName, iteration)
+                perfettoCollector.stop(uniqueName, iteration)
             }
 
             config.metrics
@@ -138,32 +143,55 @@
                 .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()
     }
 }
 
+/**
+ * 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 }
+}
+
 enum class StartupMode {
     /**
      * Startup from scratch - app's process is not alive, and must be started in addition to
@@ -193,13 +221,17 @@
 }
 
 fun startupMacrobenchmark(
-    benchmarkName: String,
+    uniqueName: String,
+    className: String,
+    testName: String,
     config: MacrobenchmarkConfig,
     startupMode: StartupMode,
     performStartup: MacrobenchmarkScope.() -> Unit
 ) {
     macrobenchmark(
-        benchmarkName = benchmarkName,
+        uniqueName = uniqueName,
+        className = className,
+        testName = testName,
         config = config,
         setupBlock = { firstIterAfterCompile ->
             if (startupMode == StartupMode.COLD) {
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 a5edac8..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,7 +24,7 @@
  * JUnit rule for benchmarking large app operations like startup.
  */
 class MacrobenchmarkRule : TestRule {
-    lateinit var benchmarkName: String
+    lateinit var currentDescription: Description
 
     fun measureRepeated(
         config: MacrobenchmarkConfig,
@@ -32,7 +32,9 @@
         measureBlock: MacrobenchmarkScope.() -> Unit
     ) {
         macrobenchmark(
-            benchmarkName = benchmarkName,
+            uniqueName = currentDescription.toUniqueName(),
+            className = currentDescription.className,
+            testName = currentDescription.methodName,
             config = config,
             launchWithClearTask = true,
             setupBlock = setupBlock,
@@ -46,7 +48,9 @@
         performStartup: MacrobenchmarkScope.() -> Unit
     ) {
         startupMacrobenchmark(
-            benchmarkName = benchmarkName,
+            uniqueName = currentDescription.toUniqueName(),
+            className = currentDescription.className,
+            testName = currentDescription.methodName,
             config = config,
             startupMode = startupMode,
             performStartup = performStartup
@@ -55,7 +59,7 @@
 
     override fun apply(base: Statement, description: Description) = object : Statement() {
         override fun evaluate() {
-            benchmarkName = description.toUniqueName()
+            currentDescription = description
             base.evaluate()
         }
     }
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 c129ddc..6c009b3 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -71,8 +71,9 @@
     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-rc01")
@@ -83,7 +84,7 @@
     val MEDIAROUTER = Version("1.3.0-alpha01")
     val NAVIGATION = Version("2.4.0-alpha01")
     val NAVIGATION_COMPOSE = Version("1.0.0-alpha04")
-    val PAGING = Version("3.0.0-alpha10")
+    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")
@@ -96,10 +97,11 @@
     val ROOM = Version("2.3.0-alpha04")
     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")
@@ -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 e764c62..9c6b2c0 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -110,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 d8ba393..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"
@@ -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 beaed73..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",
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/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/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
index 442db04..1ad9830 100644
--- 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
@@ -21,6 +21,7 @@
 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
@@ -30,12 +31,15 @@
 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].
  */
@@ -88,4 +92,9 @@
 
     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
index 3174c2c..d3dcff7 100644
--- 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
@@ -28,13 +28,11 @@
 import androidx.camera.core.impl.CameraInternal
 import androidx.camera.core.impl.LiveDataObservable
 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
 import kotlinx.atomicfu.atomic
 import javax.inject.Inject
 
-internal val defaultQuirks = Quirks(emptyList())
 internal val cameraAdapterIds = atomic(0)
 
 /**
@@ -58,11 +56,6 @@
         // TODO: Consider preloading the list of camera ids and metadata.
     }
 
-    override fun getCameraQuirks(): Quirks {
-        warn { "TODO: Quirks are not yet supported." }
-        return defaultQuirks
-    }
-
     // Load / unload methods
     override fun open() {
         debug { "$this#open" }
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/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/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/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/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/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 7adafc2..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.
      *
@@ -203,10 +198,9 @@
         try {
             CameraCharacteristicsCompat cameraCharacteristicsCompat =
                     mCameraManager.getCameraCharacteristicsCompat(cameraId);
-            mCameraQuirks = CameraQuirks.get(cameraId, cameraCharacteristicsCompat);
             mCameraControlInternal = new Camera2CameraControlImpl(cameraCharacteristicsCompat,
                     executorScheduler, mExecutor, new ControlUpdateListenerInternal(),
-                    mCameraQuirks);
+                    cameraInfoImpl.getCameraQuirks());
             mCameraInfoInternal = cameraInfoImpl;
             mCameraInfoInternal.linkWithCameraControl(mCameraControlInternal);
         } catch (CameraAccessExceptionCompat e) {
@@ -880,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 43c735b..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
@@ -26,6 +26,7 @@
 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;
@@ -36,6 +37,7 @@
 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;
@@ -80,6 +82,9 @@
     @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.
@@ -89,6 +94,7 @@
         mCameraId = Preconditions.checkNotNull(cameraId);
         mCameraCharacteristicsCompat = cameraCharacteristicsCompat;
         mCamera2CameraInfo = new Camera2CameraInfo(this);
+        mCameraQuirks = CameraQuirks.get(cameraId, cameraCharacteristicsCompat);
     }
 
     /**
@@ -336,6 +342,13 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @NonNull
+    @Override
+    public Quirks getCameraQuirks() {
+        return mCameraQuirks;
+    }
+
     /**
      * Gets the implementation of {@link Camera2CameraInfo}.
      */
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 0ec8db2..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(),
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 368e5fe..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(),
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/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/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 ea5eea6..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, null);
+                        cameraThreadConfig, availableCamerasLimiter);
                 CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
                         mCameraXConfig.getDeviceSurfaceManagerProvider(null);
                 if (surfaceManagerProvider == 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/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/CameraFactory.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
index 3efbda6..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
@@ -40,14 +40,14 @@
          *
          * @param context the android context
          * @param threadConfig the thread config to run the camera operations
-         * @param availableCamerasSelector a CameraSelector used to specify which cameras will be
+         * @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,
-                @Nullable CameraSelector availableCamerasSelector) throws InitializationException;
+                @Nullable CameraSelector availableCamerasLimiter) throws InitializationException;
     }
 
     /**
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/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-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/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/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/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 60456c9..57591a5 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,38 +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);
     field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
   }
 
-  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;
-  }
-
   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<?>);
@@ -54,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 {
@@ -90,38 +80,19 @@
   }
 
   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 OnCheckedChangeListenerWrapper {
-    method public void onCheckedChange(boolean, androidx.car.app.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.OnDoneCallback);
-  }
-
   public interface OnScreenResultCallback {
     method public void onScreenResult(Object?);
   }
 
-  public interface OnSelectedListenerWrapper {
-    method public void onSelected(int, androidx.car.app.OnDoneCallback);
-  }
-
   public abstract class Screen implements androidx.lifecycle.LifecycleOwner {
     ctor protected Screen(androidx.car.app.CarContext);
     method public final void finish();
@@ -145,14 +116,11 @@
     method public void remove(androidx.car.app.Screen);
   }
 
-  public interface SearchListener {
-    method public void onSearchSubmitted(String);
-    method public void onSearchTextChanged(String);
-  }
-
-  public interface SearchListenerWrapper {
-    method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
-    method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+  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 {
@@ -176,6 +144,14 @@
 
 }
 
+package androidx.car.app.annotations {
+
+  @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();
+  }
+
+}
+
 package androidx.car.app.model {
 
   public final class Action {
@@ -345,7 +321,7 @@
     method public int getImageType();
     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
@@ -357,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?);
   }
 
@@ -389,8 +365,8 @@
     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.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
-    method public androidx.car.app.OnSelectedListenerWrapper? getOnSelectedListener();
+    method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+    method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
     method public int getSelectedIndex();
   }
 
@@ -442,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();
@@ -474,6 +450,10 @@
     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();
   }
@@ -483,9 +463,17 @@
     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();
   }
@@ -603,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.SearchListenerWrapper getSearchListener();
+    method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
     method public boolean isLoading();
     method public boolean isShowKeyboardByDefault();
   }
@@ -626,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();
@@ -659,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.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+    method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
     method public boolean isChecked();
   }
 
@@ -771,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();
   }
 
 }
@@ -942,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?);
   }
 
@@ -963,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?);
   }
@@ -982,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?);
   }
 
@@ -1050,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);
   }
 
 }
@@ -1067,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);
   }
@@ -1114,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 60456c9..57591a5 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,38 +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);
     field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
   }
 
-  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;
-  }
-
   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<?>);
@@ -54,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 {
@@ -90,38 +80,19 @@
   }
 
   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 OnCheckedChangeListenerWrapper {
-    method public void onCheckedChange(boolean, androidx.car.app.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.OnDoneCallback);
-  }
-
   public interface OnScreenResultCallback {
     method public void onScreenResult(Object?);
   }
 
-  public interface OnSelectedListenerWrapper {
-    method public void onSelected(int, androidx.car.app.OnDoneCallback);
-  }
-
   public abstract class Screen implements androidx.lifecycle.LifecycleOwner {
     ctor protected Screen(androidx.car.app.CarContext);
     method public final void finish();
@@ -145,14 +116,11 @@
     method public void remove(androidx.car.app.Screen);
   }
 
-  public interface SearchListener {
-    method public void onSearchSubmitted(String);
-    method public void onSearchTextChanged(String);
-  }
-
-  public interface SearchListenerWrapper {
-    method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
-    method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+  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 {
@@ -176,6 +144,14 @@
 
 }
 
+package androidx.car.app.annotations {
+
+  @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();
+  }
+
+}
+
 package androidx.car.app.model {
 
   public final class Action {
@@ -345,7 +321,7 @@
     method public int getImageType();
     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
@@ -357,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?);
   }
 
@@ -389,8 +365,8 @@
     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.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
-    method public androidx.car.app.OnSelectedListenerWrapper? getOnSelectedListener();
+    method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+    method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
     method public int getSelectedIndex();
   }
 
@@ -442,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();
@@ -474,6 +450,10 @@
     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();
   }
@@ -483,9 +463,17 @@
     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();
   }
@@ -603,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.SearchListenerWrapper getSearchListener();
+    method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
     method public boolean isLoading();
     method public boolean isShowKeyboardByDefault();
   }
@@ -626,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();
@@ -659,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.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+    method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
     method public boolean isChecked();
   }
 
@@ -771,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();
   }
 
 }
@@ -942,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?);
   }
 
@@ -963,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?);
   }
@@ -982,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?);
   }
 
@@ -1050,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);
   }
 
 }
@@ -1067,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);
   }
@@ -1114,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 60456c9..57591a5 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,38 +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);
     field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
   }
 
-  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;
-  }
-
   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<?>);
@@ -54,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 {
@@ -90,38 +80,19 @@
   }
 
   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 OnCheckedChangeListenerWrapper {
-    method public void onCheckedChange(boolean, androidx.car.app.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.OnDoneCallback);
-  }
-
   public interface OnScreenResultCallback {
     method public void onScreenResult(Object?);
   }
 
-  public interface OnSelectedListenerWrapper {
-    method public void onSelected(int, androidx.car.app.OnDoneCallback);
-  }
-
   public abstract class Screen implements androidx.lifecycle.LifecycleOwner {
     ctor protected Screen(androidx.car.app.CarContext);
     method public final void finish();
@@ -145,14 +116,11 @@
     method public void remove(androidx.car.app.Screen);
   }
 
-  public interface SearchListener {
-    method public void onSearchSubmitted(String);
-    method public void onSearchTextChanged(String);
-  }
-
-  public interface SearchListenerWrapper {
-    method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
-    method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+  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 {
@@ -176,6 +144,14 @@
 
 }
 
+package androidx.car.app.annotations {
+
+  @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();
+  }
+
+}
+
 package androidx.car.app.model {
 
   public final class Action {
@@ -345,7 +321,7 @@
     method public int getImageType();
     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
@@ -357,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?);
   }
 
@@ -389,8 +365,8 @@
     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.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
-    method public androidx.car.app.OnSelectedListenerWrapper? getOnSelectedListener();
+    method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+    method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
     method public int getSelectedIndex();
   }
 
@@ -442,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();
@@ -474,6 +450,10 @@
     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();
   }
@@ -483,9 +463,17 @@
     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();
   }
@@ -603,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.SearchListenerWrapper getSearchListener();
+    method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
     method public boolean isLoading();
     method public boolean isShowKeyboardByDefault();
   }
@@ -626,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();
@@ -659,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.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+    method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
     method public boolean isChecked();
   }
 
@@ -771,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();
   }
 
 }
@@ -942,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?);
   }
 
@@ -963,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?);
   }
@@ -982,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?);
   }
 
@@ -1050,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);
   }
 
 }
@@ -1067,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);
   }
@@ -1114,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/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/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index 7c3c854..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;
@@ -78,39 +77,34 @@
  * 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;
     }
 
@@ -118,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
@@ -149,94 +143,21 @@
     }
 
     /**
-     * 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#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>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}.
@@ -245,7 +166,7 @@
      * 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,
@@ -274,69 +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)) {
                 Log.d(TAG, "Executing onAutoDriveEnabled");
-                runOnMain(mCarContext.getCarService(NavigationManager.class)::onAutoDriveEnabled);
+                runOnMain(() -> {
+                    if (mCurrentSession != null) {
+                        mCurrentSession.getCarContext().getCarService(
+                                NavigationManager.class).onAutoDriveEnabled();
+                    }
+                });
             }
         }
     }
@@ -347,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.
@@ -367,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();
@@ -388,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);
@@ -400,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
@@ -448,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:
@@ -467,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
@@ -481,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.
@@ -504,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 11837d2..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;
 
@@ -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..7174e18 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/ScreenManager.java b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
index 56b3365..7dafbc2 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() {
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/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index 32e9482..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
@@ -98,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. */
@@ -208,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;
         }
 
@@ -221,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) {
@@ -314,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/ItemList.java b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
index 367931b..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,11 +26,7 @@
 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.OnItemVisibilityChangedListenerWrapper;
-import androidx.car.app.OnSelectedListenerWrapper;
 import androidx.car.app.WrappedRuntimeException;
 import androidx.car.app.utils.RemoteUtils;
 
@@ -237,7 +233,7 @@
          * @see #setSelectedIndex(int)
          */
         @NonNull
-        @SuppressLint({"ExecutorRegistration"})
+        @SuppressLint("ExecutorRegistration")
         public Builder setOnSelectedListener(@Nullable OnSelectedListener onSelectedListener) {
             this.mOnSelectedListener =
                     onSelectedListener == null ? null : createOnSelectedListener(
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 630ead2..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
@@ -93,7 +93,7 @@
     }
 
     @Nullable
-    public ActionList getActionList() {
+    public ActionList getActions() {
         return mActionList;
     }
 
@@ -273,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/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/OnCheckedChangeListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeListenerWrapper.java
index 4aaca10..603323c 100644
--- a/car/app/app/src/main/java/androidx/car/app/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;
+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/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/OnItemVisibilityChangedListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedListenerWrapper.java
index 5d7175f6..62e3e5b 100644
--- a/car/app/app/src/main/java/androidx/car/app/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;
+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/OnSelectedListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
similarity index 94%
rename from car/app/app/src/main/java/androidx/car/app/OnSelectedListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
index 812d211..92e3006 100644
--- a/car/app/app/src/main/java/androidx/car/app/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;
+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.
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 513cac3..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,8 +18,6 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.SuppressLint;
-
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -52,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;
     }
 
@@ -157,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/PlaceListMapTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
index ada9d74..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
@@ -220,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>
          *
@@ -242,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) {
@@ -308,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.
@@ -328,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) {
@@ -350,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 1099af0..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
@@ -302,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.
          *
diff --git a/car/app/app/src/main/java/androidx/car/app/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/SearchListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/SearchListenerWrapper.java
index 050ac89..d331059 100644
--- a/car/app/app/src/main/java/androidx/car/app/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;
+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 18976ce..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,11 +28,8 @@
 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.SearchListenerWrapper;
 import androidx.car.app.WrappedRuntimeException;
 import androidx.car.app.utils.RemoteUtils;
 
@@ -49,6 +46,29 @@
  * 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
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 f704b7a..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,9 +25,7 @@
 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.OnCheckedChangeListenerWrapper;
 import androidx.car.app.OnDoneCallback;
 import androidx.car.app.WrappedRuntimeException;
 import androidx.car.app.utils.RemoteUtils;
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 787fa0e..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,7 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.SuppressLint;
+import android.os.Looper;
 import android.util.Log;
 
 import androidx.annotation.MainThread;
@@ -38,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.
@@ -51,17 +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 static final String TAG = "NavigationManager";
 
-    private final INavigationManager.Stub mNavigationmanager;
+    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;
 
@@ -74,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
@@ -86,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) {
@@ -118,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;
     }
 
     /**
@@ -146,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.
      *
@@ -162,7 +206,7 @@
         if (mIsNavigating) {
             return;
         }
-        if (mListener == null) {
+        if (mNavigationManagerListener == null) {
             throw new IllegalStateException("No listener has been set");
         }
         mIsNavigating = true;
@@ -209,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);
     }
 
     /**
@@ -221,7 +266,7 @@
     @RestrictTo(LIBRARY)
     @NonNull
     public INavigationManager.Stub getIInterface() {
-        return mNavigationmanager;
+        return mNavigationManager;
     }
 
     /**
@@ -231,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.
@@ -255,9 +301,11 @@
     public void onAutoDriveEnabled() {
         checkMainThread();
         mIsAutoDriveEnabled = true;
-        if (mListener != null) {
+        if (mNavigationManagerListener != null) {
             Log.d(TAG, "Executing onAutoDriveEnabled");
-            mListener.onAutoDriveEnabled();
+            requireNonNull(mNavigationManagerListenerExecutor).execute(() -> {
+                mNavigationManagerListener.onAutoDriveEnabled();
+            });
         } else {
             Log.w(TAG, "NavigationManagerListener not set, skipping onAutoDriveEnabled");
         }
@@ -266,14 +314,17 @@
     /** @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/PlaceListNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
index 48927a8..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
@@ -58,7 +58,7 @@
  *
  * <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>
@@ -191,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;
         }
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 f64fa55..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
@@ -68,7 +68,7 @@
  *
  * <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>
@@ -218,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/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 168a094..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;
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 95%
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 6dd5a7f..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
@@ -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 f442e0d..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 onGetTemplate() {
-                                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 98%
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 38432dd..4e55c08 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;
@@ -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,7 +218,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void pushForResult_addsToStack_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager,
@@ -251,7 +245,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void pushForResult_screenSetsResult_firstScreenGetsCalled() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
 
@@ -269,7 +262,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void pushForResult_screenSetsResult_firstScreenGetsCalled_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager,
@@ -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);
@@ -675,7 +662,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popToRoot_pops_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -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);
@@ -764,7 +749,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void popTo_multipleToPop_withResult_pops_callsProperLifecycleMethods() {
         mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
         InOrder inOrder =
@@ -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()
@@ -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()
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 93%
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 7ab1650..98e0f42 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,20 +31,19 @@
 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;
 
@@ -54,7 +53,6 @@
     private Screen mScreen;
 
     @Before
-    @UiThreadTest
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mCarContext =
@@ -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,28 +110,24 @@
     }
 
     @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);
 
@@ -151,7 +142,6 @@
     }
 
     @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 100%
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
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 90%
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 d946284..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.
      *
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 95%
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 b0a3a200..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() {
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 82%
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 93a2562..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.OnDoneCallback;
-import androidx.car.app.test.R;
+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 68e0a3f..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
@@ -28,25 +28,23 @@
 import android.os.RemoteException;
 
 import androidx.car.app.OnDoneCallback;
-import androidx.test.annotation.UiThreadTest;
-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 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/androidTest/java/androidx/car/app/model/GridTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
index 5548996..6078ee3 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
@@ -23,15 +23,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 GridTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class GridTemplateTest {
     @Test
     public void createInstance_emptyList_notLoading_throws() {
@@ -162,7 +162,7 @@
         ItemList itemList = ItemList.builder().build();
 
         GridTemplate template =
-                GridTemplate.builder().setTitle("Title").setSingleList(itemList).build();
+                GridTemplate.builder().setTitle("Title 1").setSingleList(itemList).build();
 
         assertThat(template)
                 .isNotEqualTo(
@@ -170,7 +170,8 @@
                                 .setTitle("Title")
                                 .setSingleList(
                                         ItemList.builder().addItem(
-                                                GridItem.builder().setImage(BACK).build()).build())
+                                                GridItem.builder().setTitle("Title 2").setImage(
+                                                        BACK).build()).build())
                                 .build());
     }
 
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java b/car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
index 90df854..e306d1b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
@@ -35,9 +35,6 @@
 import androidx.car.app.WrappedRuntimeException;
 import androidx.car.app.model.ItemList.OnItemVisibilityChangedListener;
 import androidx.car.app.model.ItemList.OnSelectedListener;
-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;
@@ -48,12 +45,14 @@
 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}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ItemListTest {
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
@@ -85,8 +84,8 @@
 
     @Test
     public void createGridItems() {
-        GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
-        GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
+        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();
@@ -114,7 +113,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void setSelectable() throws RemoteException {
         OnSelectedListener mockListener = mock(OnSelectedListener.class);
         ItemList itemList =
@@ -163,7 +161,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void setOnItemVisibilityChangeListener_triggerListener() {
         OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
         ItemList list =
@@ -185,7 +182,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void setOnItemVisibilityChangeListener_triggerListenerWithFailure() {
         OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
         ItemList list =
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/androidTest/java/androidx/car/app/model/ListTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java
index 7bf4812..30d52b0 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.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 ListTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class ListTemplateTest {
     @Test
     public void createInstance_emptyList_notLoading_Throws() {
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 97%
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 2d6a0d4..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
@@ -27,17 +27,17 @@
 import android.util.Log;
 
 import androidx.core.graphics.drawable.IconCompat;
-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";
@@ -82,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();
     }
 
@@ -121,7 +121,7 @@
                 Log.getStackTraceString(exception));
         assertThat(template.getIcon()).isEqualTo(icon);
         assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
-        assertThat(template.getActionList().getList()).containsExactly(action);
+        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/model/OnClickListenerWrapperTest.java b/car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
similarity index 88%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
index 5db449f..3afb7ee 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
@@ -24,9 +24,6 @@
 import static org.mockito.Mockito.verify;
 
 import androidx.car.app.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;
@@ -34,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();
@@ -45,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 97%
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 f2ffb88..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
@@ -21,15 +21,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 PaneTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PaneTemplateTest {
 
     @Test
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 95%
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 a7bd7aa..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,20 +20,19 @@
 
 import static org.junit.Assert.assertThrows;
 
-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() {
@@ -78,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
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 90%
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 8b9d03a..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
@@ -24,9 +24,6 @@
 import android.os.RemoteException;
 
 import androidx.car.app.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;
@@ -34,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();
@@ -46,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 98%
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 20ce3a7..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
@@ -25,15 +25,15 @@
 
 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 PlaceListMapTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
 public class PlaceListMapTemplateTest {
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private final DistanceSpan mDistanceSpan =
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 93%
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 73a62dd..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
@@ -26,19 +26,17 @@
 import static org.mockito.Mockito.verify;
 
 import androidx.car.app.OnDoneCallback;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
+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() {
@@ -98,7 +96,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void clickListener() {
         OnClickListener onClickListener = mock(OnClickListener.class);
         Row row = Row.builder().setTitle("Title").setOnClickListener(onClickListener).build();
@@ -145,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();
     }
 
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 97%
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 9a754f2..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
@@ -27,12 +27,8 @@
 import android.os.RemoteException;
 
 import androidx.car.app.OnDoneCallback;
-import androidx.car.app.SearchListener;
 import androidx.car.app.TestUtils;
 import androidx.car.app.WrappedRuntimeException;
-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 93%
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 dad2cbd..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
@@ -26,9 +26,6 @@
 import androidx.car.app.OnDoneCallback;
 import androidx.car.app.WrappedRuntimeException;
 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 98%
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 c5d2636..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
@@ -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
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 98%
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 2cbd5c4..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
@@ -29,17 +29,17 @@
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.Distance;
 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();
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 98%
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 844c094..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
@@ -38,15 +38,15 @@
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Toggle;
 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 =
@@ -60,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
@@ -70,7 +70,7 @@
                 () ->
                         PlaceListNavigationTemplate.builder()
                                 .setTitle("Title")
-                                .setIsLoading(true)
+                                .setLoading(true)
                                 .setItemList(ItemList.builder().build())
                                 .build());
     }
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 95%
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 57a4723..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
@@ -36,19 +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.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 =
@@ -62,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
@@ -71,7 +68,7 @@
                 IllegalStateException.class,
                 () -> RoutePreviewNavigationTemplate.builder()
                         .setTitle("Title")
-                        .setIsLoading(true)
+                        .setLoading(true)
                         .setItemList(
                                 TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
                         .build());
@@ -127,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();
     }
 
@@ -185,7 +182,6 @@
     }
 
     @Test
-    @UiThreadTest
     public void setOnNavigateAction() {
         OnClickListener mockListener = mock(OnClickListener.class);
         RoutePreviewNavigationTemplate template =
@@ -216,7 +212,7 @@
                         .build());
 
         // Positive case
-        RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+        RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
     }
 
     @Test
@@ -234,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()
@@ -271,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(() -> {
                 })
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 98%
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 7b51591..ca589ec 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
@@ -27,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;
@@ -41,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");
@@ -66,7 +65,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 26)
     public void create_duration() {
         ZonedDateTime arrivalTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
         Duration remainingTime = Duration.ofHours(10);
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 92%
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 707e112..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);
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 f006161..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
@@ -32,6 +32,7 @@
 
 import androidx.annotation.Nullable;
 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 98%
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 1376932..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
@@ -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);
 
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 100%
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
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 100%
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
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 100%
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
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..4455664 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()) {
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..36e0942 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,12 @@
 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.dp
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun SuspendAnimationDemo() {
     var animStateX by remember {
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..289b452 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,7 +42,6 @@
 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
@@ -58,9 +57,10 @@
 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 +72,8 @@
         Button(
             onClick = {
                 index = (index + 1) % pastelColors.size
+                dismissState.alpha = 1f
+                dismissState.offset = 0f
             },
             modifier = Modifier.align(Alignment.CenterHorizontally)
         ) {
@@ -80,21 +82,13 @@
     }
 }
 
-@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) {
@@ -106,7 +100,7 @@
                 mutatorMutex.mutate(MutatePriority.UserInput) {
                     handlePointerInput {
                         verticalDrag(pointerId) {
-                            updateOffset(offset + it.positionChange().y)
+                            updateOffset(dismissState.offset + it.positionChange().y)
                             velocityTracker.addPosition(
                                 it.current.uptime,
                                 it.current.position
@@ -120,9 +114,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 +136,12 @@
                 }
             }
         }
-    }.offset(y = { offset }).graphicsLayer(alpha = alpha)
+    }.offset(y = { dismissState.offset }).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/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 b12654e..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,31 +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",
-        2000 to "1.0.0-alpha09"
-    )
+    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 = 2000
+        /**
+         * 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-alpha09"
-    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 23699a4..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
@@ -72,7 +72,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.graphics.graphicsLayer
-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
@@ -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) }
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 163625a..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
@@ -36,7 +36,7 @@
 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
@@ -317,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)
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 fb477e1..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
@@ -22,31 +22,31 @@
 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/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/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt
index 7a9fe53..089ff61 100644
--- 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
@@ -70,7 +70,7 @@
         }
         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(root.width, root.height), alignSize.value)
@@ -111,7 +111,7 @@
         }
         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(root.width, root.height), alignSize.value)
@@ -157,7 +157,7 @@
         }
         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(
@@ -231,7 +231,7 @@
         }
         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(size, size), alignSize.value)
@@ -472,7 +472,7 @@
         }
         positionedLatch.await(1, TimeUnit.SECONDS)
 
-        val root = findOwnerView()
+        val root = findComposeView()
         waitForDraw(root)
 
         assertEquals(IntSize(childSizeIpx, childSizeIpx), childSize.value)
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/LayoutPaddingTest.kt
index 06c69bc..cd5004b 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/LayoutPaddingTest.kt
@@ -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/LayoutSizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt
index 6e71283..9130a42 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/LayoutSizeTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.layout
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.emptyContent
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.Modifier
@@ -1283,10 +1284,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 +1302,7 @@
             }
         }
         assertTrue(latch.await(1, TimeUnit.SECONDS))
+        assertEquals(expectedConstraints, actualConstraints)
     }
 
     private fun verifyIntrinsicMeasurements(expandedModifier: Modifier) = with(density) {
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/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/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/api/current.txt b/compose/foundation/foundation/api/current.txt
index ddb6503..0d949bc 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -232,18 +232,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-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method 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 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 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 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 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 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-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? 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 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);
   }
 
   public final class DraggableKt {
@@ -251,7 +251,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 +265,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, 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);
+    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 {
@@ -293,9 +293,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.HandlePointerInputScope, 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.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
   }
 
   public final class ZoomableController {
@@ -320,15 +320,15 @@
   }
 
   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 {
@@ -340,9 +340,29 @@
     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 {
   }
 
@@ -356,6 +376,7 @@
     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);
@@ -363,6 +384,7 @@
     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;
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index ddb6503..0d949bc 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -232,18 +232,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-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method 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 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 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 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 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 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-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? 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 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);
   }
 
   public final class DraggableKt {
@@ -251,7 +251,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 +265,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, 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);
+    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 {
@@ -293,9 +293,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.HandlePointerInputScope, 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.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
   }
 
   public final class ZoomableController {
@@ -320,15 +320,15 @@
   }
 
   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 {
@@ -340,9 +340,29 @@
     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 {
   }
 
@@ -356,6 +376,7 @@
     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);
@@ -363,6 +384,7 @@
     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;
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index ddb6503..0d949bc 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -232,18 +232,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-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+    method 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 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 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 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 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 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-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? 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 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);
   }
 
   public final class DraggableKt {
@@ -251,7 +251,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 +265,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, 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);
+    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 {
@@ -293,9 +293,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.HandlePointerInputScope, 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.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
   }
 
   public final class ZoomableController {
@@ -320,15 +320,15 @@
   }
 
   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 {
@@ -340,9 +340,29 @@
     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 {
   }
 
@@ -356,6 +376,7 @@
     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);
@@ -363,6 +384,7 @@
     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;
   }
 
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..d3d049e 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
@@ -18,8 +18,11 @@
 
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
+import androidx.compose.foundation.ScrollableColumn
 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 +31,14 @@
 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.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.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 +50,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 +75,27 @@
     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() },
     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 +111,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 +180,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 +205,10 @@
 
 @Composable
 private fun LazyRowItemsDemo() {
-    LazyRowFor(items = (1..1000).toList()) {
-        Square(it)
+    LazyRow {
+        items((1..1000).toList()) {
+            Square(it)
+        }
     }
 }
 
@@ -218,11 +227,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 +243,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 +260,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 +314,165 @@
             }
         }
     }
-}
\ 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)
+        }
+    }
+}
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 1310c48..a16da6a 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
@@ -51,7 +51,6 @@
 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
@@ -107,7 +106,6 @@
 /**
  * Gesture detector for tap, double-tap, and long-press.
  */
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun CoroutineTapDemo() {
     var tapHue by remember { mutableStateOf(randomHue()) }
@@ -212,7 +210,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun TouchSlopDragGestures() {
     Column {
@@ -290,7 +287,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun OrientationLockDragGestures() {
     var size by remember { mutableStateOf(IntSize.Zero) }
@@ -335,7 +331,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun Drag2DGestures() {
     var size by remember { mutableStateOf(IntSize.Zero) }
@@ -365,7 +360,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 fun MultitouchArea(
     text: String,
@@ -440,7 +434,6 @@
  * 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(
@@ -458,7 +451,6 @@
  * 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(
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..68e282c 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,7 +25,6 @@
 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
@@ -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,
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..85f8cb8 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,7 +48,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.graphics.Color
 import androidx.compose.ui.input.pointer.consumePositionChange
 import androidx.compose.ui.input.pointer.pointerInput
@@ -57,7 +56,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun AwaitHorizontalDragOrCancellationSample() {
@@ -102,7 +100,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun HorizontalDragSample() {
@@ -146,7 +143,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectHorizontalDragGesturesSample() {
@@ -174,7 +170,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun AwaitVerticalDragOrCancellationSample() {
@@ -219,7 +214,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun VerticalDragSample() {
@@ -263,7 +257,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectVerticalDragGesturesSample() {
@@ -291,7 +284,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun AwaitDragOrCancellationSample() {
@@ -348,7 +340,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DragSample() {
@@ -404,7 +395,6 @@
     }
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectDragGesturesSample() {
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/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 a9c5f93..02da81a 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,12 +37,10 @@
 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
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun DetectMultitouchGestures() {
@@ -72,7 +70,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculateRotation() {
@@ -97,7 +94,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculateZoom() {
@@ -121,7 +117,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculatePan() {
@@ -149,7 +144,6 @@
     )
 }
 
-@OptIn(ExperimentalPointerInput::class)
 @Composable
 @Sampled
 fun CalculateCentroidSize() {
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 591300c..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
@@ -58,6 +58,7 @@
 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
@@ -302,6 +303,7 @@
     }
 
     @Test
+    @LargeTest
     fun testImageScalesNonuniformly() {
         val imageComposableWidth = imageWidth * 3
         val imageComposableHeight = imageHeight * 7
@@ -524,6 +526,7 @@
     }
 
     @Test
+    @LargeTest
     fun testPainterResourceWithImage() {
         val testTag = "testTag"
         var imageColor = Color.Black
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..6b3288e 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
@@ -32,6 +32,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 +52,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
@@ -511,7 +516,6 @@
                 Box(
                     contentAlignment = Alignment.Center,
                     modifier = Modifier
-                        .testTag(scrollableBoxTag)
                         .preferredSize(300.dp)
                         .scrollable(
                             controller = outerState,
@@ -519,15 +523,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 +619,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..c617a2f 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,7 +27,6 @@
 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.graphics.Color
@@ -54,7 +53,7 @@
 import java.util.concurrent.TimeUnit
 
 @LargeTest
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
 class TextFieldCursorTest {
 
     @get:Rule
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..cf8a3c8 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,7 +22,6 @@
 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
@@ -37,7 +36,6 @@
 import org.junit.runner.RunWith
 
 @LargeTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class TextFieldFocusTest {
     @get:Rule
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 3ee3b91..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
@@ -247,6 +248,7 @@
     }
 
     @Test
+    @LargeTest
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun testTextField_horizontal_scrolledAndClipped() {
         val scrollerPosition = TextFieldScrollerPosition()
@@ -294,6 +296,7 @@
     }
 
     @Test
+    @LargeTest
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun testTextField_vertical_scrolledAndClipped() {
         val scrollerPosition = TextFieldScrollerPosition()
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..d231d22 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,7 +37,6 @@
 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.graphics.Color
@@ -97,10 +96,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalFoundationApi::class)
 class TextFieldTest {
     @get:Rule
     val rule = createComposeRule()
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 bbd257b..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
+++ /dev/null
@@ -1,1136 +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.animation.core.snap
-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)
-        }
-    }
-
-    @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 {
-            LazyColumnFor(
-                items = items,
-                state = rememberLazyListState().also { state = it },
-                modifier = Modifier.height(itemSizeMinusOne).testTag(LazyColumnForTag)
-            ) {
-                Spacer(
-                    if (it == 0) {
-                        Modifier.width(30.dp).height(itemSizeMinusOne)
-                    } else {
-                        Modifier.width(20.dp).height(itemSize)
-                    }
-                )
-            }
-        }
-
-        state.scrollBy(itemSize)
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .assertWidthIsEqualTo(20.dp)
-    }
-
-    @Test
-    fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
-        val items = (0..2).toList()
-        val itemSize = with(rule.density) { 30.toDp() }
-        lateinit var state: LazyListState
-        rule.setContent {
-            LazyColumnFor(
-                items = items,
-                state = rememberLazyListState().also { state = it },
-                modifier = Modifier.height(itemSize * 1.75f).testTag(LazyColumnForTag)
-            ) {
-                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(LazyColumnForTag)
-            .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/LazyColumnTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
index 7e629eb3..d0c6400 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,77 @@
 
 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.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 +131,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 +176,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/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..8b33fbf 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
@@ -68,11 +68,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 +79,9 @@
                     bottom = largePaddingSize
                 )
             ) {
-                Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+                items(listOf(1)) {
+                    Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+                }
             }
         }
 
@@ -101,17 +102,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 +129,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 +170,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 +205,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 +249,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 +281,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 +289,9 @@
                         bottom = 8.dp
                     )
                 ) {
-                    Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    items(listOf(1)) {
+                        Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    }
                 }
             }
         }
@@ -306,8 +313,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 +321,8 @@
                         bottom = 8.dp
                     )
                 ) {
+                    items(listOf(0)) {
+                    }
                 }
             }
         }
@@ -333,11 +341,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 +352,9 @@
                     end = largePaddingSize
                 )
             ) {
-                Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+                items(listOf(1)) {
+                    Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+                }
             }
         }
 
@@ -369,17 +378,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 +405,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 +446,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 +481,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 +525,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 +557,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 +565,9 @@
                         bottom = 8.dp
                     )
                 ) {
-                    Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    items(listOf(1)) {
+                        Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                    }
                 }
             }
         }
@@ -574,8 +589,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 +597,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 f4d3fd4..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
+++ /dev/null
@@ -1,903 +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.snap
-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)
-        }
-    }
-
-    @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 {
-            LazyRowFor(
-                items = items,
-                state = rememberLazyListState().also { state = it },
-                modifier = Modifier.width(itemSizeMinusOne).testTag(LazyRowForTag)
-            ) {
-                Spacer(
-                    if (it == 0) {
-                        Modifier.height(30.dp).width(itemSizeMinusOne)
-                    } else {
-                        Modifier.height(20.dp).width(itemSize)
-                    }
-                )
-            }
-        }
-
-        state.scrollBy(itemSize)
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .assertHeightIsEqualTo(20.dp)
-    }
-
-    @Test
-    fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
-        val items = (0..2).toList()
-        val itemSize = with(rule.density) { 30.toDp() }
-        lateinit var state: LazyListState
-        rule.setContent {
-            LazyRowFor(
-                items = items,
-                state = rememberLazyListState().also { state = it },
-                modifier = Modifier.width(itemSize * 1.75f).testTag(LazyRowForTag)
-            ) {
-                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(LazyRowForTag)
-            .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/LazyRowTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
index fe720d7..1ab378a 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,44 @@
 
 package androidx.compose.foundation.lazy
 
+import androidx.compose.animation.core.snap
+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 +61,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 +111,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 +156,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..d216574 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
@@ -90,12 +90,16 @@
     @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..6b681db 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,7 +17,6 @@
 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.platform.testTag
@@ -48,8 +47,7 @@
 
 @OptIn(
     ExperimentalTextApi::class,
-    InternalTextApi::class,
-    ExperimentalFocus::class
+    InternalTextApi::class
 )
 @LargeTest
 @RunWith(AndroidJUnit4::class)
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
index 8050c7c..7d2d992 100644
--- 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
@@ -23,7 +23,6 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.mutableStateOf
 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.geometry.Offset
@@ -49,7 +48,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(InternalTextApi::class, ExperimentalFocus::class)
+@OptIn(InternalTextApi::class)
 class TextFieldInteractionsTest {
 
     @get:Rule
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/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index 1570a3a..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,7 +17,7 @@
 package androidx.compose.foundation.text.selection
 
 import android.view.View
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.window.isPopupLayout
 import androidx.test.espresso.Espresso
@@ -34,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))
 }
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/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 627923d..8453061 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,7 +25,6 @@
 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.platform.debugInspectorInfo
@@ -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,
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 c30a7c6..aee281c 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
@@ -478,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()."
@@ -486,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/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 4f6652d..1477c75 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,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.HandlePointerInputScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
@@ -56,7 +55,6 @@
  * @see awaitHorizontalTouchSlopOrCancellation
  * @see awaitVerticalTouchSlopOrCancellation
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.awaitTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
@@ -124,7 +122,6 @@
  * @see horizontalDrag
  * @see verticalDrag
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.drag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit
@@ -159,7 +156,6 @@
  * @see awaitHorizontalDragOrCancellation
  * @see drag
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.awaitDragOrCancellation(
     pointerId: PointerId,
 ): PointerInputChange? {
@@ -184,7 +180,6 @@
  * @see detectVerticalDragGestures
  * @see detectHorizontalDragGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectDragGestures(
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
@@ -235,7 +230,6 @@
  * @see awaitHorizontalTouchSlopOrCancellation
  * @see awaitTouchSlopOrCancellation
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.awaitVerticalTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
@@ -265,7 +259,6 @@
  * @see horizontalDrag
  * @see drag
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.verticalDrag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit
@@ -293,7 +286,6 @@
  * @see awaitDragOrCancellation
  * @see verticalDrag
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.awaitVerticalDragOrCancellation(
     pointerId: PointerId,
 ): PointerInputChange? {
@@ -322,7 +314,6 @@
  * @see detectDragGestures
  * @see detectHorizontalDragGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectVerticalDragGestures(
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
@@ -369,7 +360,6 @@
  * @see awaitVerticalTouchSlopOrCancellation
  * @see awaitTouchSlopOrCancellation
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.awaitHorizontalTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
@@ -396,7 +386,6 @@
  * @see verticalDrag
  * @see drag
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.horizontalDrag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit
@@ -424,7 +413,6 @@
  * @see awaitVerticalDragOrCancellation
  * @see awaitDragOrCancellation
  */
-@ExperimentalPointerInput
 suspend fun HandlePointerInputScope.awaitHorizontalDragOrCancellation(
     pointerId: PointerId,
 ): PointerInputChange? {
@@ -453,7 +441,6 @@
  * @see detectVerticalDragGestures
  * @see detectDragGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectHorizontalDragGestures(
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
@@ -488,7 +475,6 @@
  * @return `true` when the gesture ended with all pointers up and `false` when the gesture
  * was canceled.
  */
-@ExperimentalPointerInput
 private suspend inline fun HandlePointerInputScope.drag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit,
@@ -522,7 +508,6 @@
  * 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(
     pointerId: PointerId,
     hasDragged: (PointerInputChange) -> Boolean
@@ -568,7 +553,6 @@
  * `null` if all pointers are raised or the position change was consumed by another gesture
  * detector.
  */
-@ExperimentalPointerInput
 private suspend inline fun HandlePointerInputScope.awaitTouchSlopOrCancellation(
     pointerId: PointerId,
     onTouchSlopReached: (PointerInputChange, Float) -> Unit,
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 d3cd6e1..d545ee0 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,7 +15,6 @@
  */
 package androidx.compose.foundation.gestures
 
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.HandlePointerInputScope
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputScope
@@ -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,14 +57,12 @@
  * 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 =
     !currentEvent.changes.fastAny { it.current.down }
 
 /**
  * Waits for all pointers to be up before returning.
  */
-@ExperimentalPointerInput
 internal suspend fun PointerInputScope.awaitAllPointersUp() {
     handlePointerInput { awaitAllPointersUp() }
 }
@@ -74,7 +70,6 @@
 /**
  * Waits for all pointers to be up before returning.
  */
-@ExperimentalPointerInput
 internal suspend fun HandlePointerInputScope.awaitAllPointersUp() {
     if (!allPointersUp()) {
         do {
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 e7188c3..ccf797d 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
@@ -48,7 +47,6 @@
  * Example Usage:
  * @sample androidx.compose.foundation.samples.DetectMultitouchGestures
  */
-@ExperimentalPointerInput
 suspend fun PointerInputScope.detectMultitouchGestures(
     panZoomLock: Boolean = false,
     onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
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..6949e0b 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
@@ -35,17 +35,25 @@
 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
@@ -258,24 +266,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 +384,7 @@
     onScrollStopped: (velocity: Float) -> Unit = {}
 ): Modifier = composed(
     factory = {
+        controller.update(orientation, reverseDirection)
         onDispose {
             controller.stopAnimation()
             controller.interactionState?.removeInteraction(Interaction.Dragged)
@@ -333,10 +403,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 +418,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 +434,7 @@
         ).mouseScrollable(
             scrollCallback,
             orientation
-        )
+        ).nestedScroll(controller.nestedScrollConnection, controller.nestedScrollDispatcher)
     },
     inspectorInfo = debugInspectorInfo {
         name = "scrollable"
@@ -398,7 +467,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/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index d9f141b..ca356ef 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,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.HandlePointerInputScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
@@ -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,
@@ -186,7 +184,6 @@
  * 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(
     requireUnconsumed: Boolean = true
 ): PointerInputChange {
@@ -208,7 +205,6 @@
  * 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? {
     while (true) {
         val event = awaitPointerEvent(PointerEventPass.Main)
@@ -233,7 +229,6 @@
 /**
  * Consumes all event changes in the [PointerEventPass.Initial] until all pointers are up.
  */
-@ExperimentalPointerInput
 private suspend fun PointerInputScope.consumeAllEventsUntilUp() {
     handlePointerInput {
         if (!allPointersUp()) {
@@ -251,7 +246,6 @@
  * 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? {
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..9da735e 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
@@ -170,17 +172,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 +206,9 @@
         state = state,
         contentPadding = contentPadding,
         verticalAlignment = verticalAlignment,
-        isVertical = false
+        horizontalArrangement = horizontalArrangement,
+        isVertical = false,
+        reverseLayout = reverseLayout
     ) { index ->
         scope.contentFor(index, this)
     }
@@ -207,20 +222,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 +259,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..2bcbed5 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,10 +16,12 @@
 
 package androidx.compose.foundation.lazy
 
+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
@@ -27,7 +29,7 @@
 
 /**
  * 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
@@ -52,7 +54,10 @@
         modifier = modifier,
         state = state,
         contentPadding = contentPadding,
-        isVertical = true
+        isVertical = true,
+        horizontalAlignment = Alignment.Start,
+        verticalArrangement = Arrangement.Top,
+        reverseLayout = false
     ) { rowIndex ->
         {
             GridRow {
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 10aaf67..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
@@ -18,10 +18,14 @@
 
 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,25 +33,41 @@
 import androidx.compose.ui.layout.SubcomposeLayout
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.util.fastForEach
 
+@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
@@ -55,8 +75,7 @@
         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()
@@ -71,21 +90,32 @@
         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,
             this,
             cachingItemContentFactory
-        ) { placeables ->
+        ) { 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
+                endContentPadding = endContentPaddingPx,
+                spacing = spacing
             )
         }
 
@@ -102,18 +132,33 @@
 
         state.applyMeasureResult(measureResult)
 
-        val layoutWidth = constraints.constrainWidth(
-            if (isVertical) measureResult.crossAxisSize else measureResult.mainAxisSize
+        layoutLazyList(
+            constraints,
+            isVertical,
+            verticalArrangement,
+            horizontalArrangement,
+            measureResult,
+            reverseLayout
         )
-        val layoutHeight = constraints.constrainHeight(
-            if (isVertical) measureResult.mainAxisSize else measureResult.crossAxisSize
-        )
-        layout(layoutWidth, layoutHeight) {
-            var currentMainAxis = measureResult.itemsScrollOffset
-            measureResult.items.fastForEach {
-                it.place(this, layoutWidth, layoutHeight, currentMainAxis)
-                currentMainAxis += it.mainAxisSize
-            }
+    }
+}
+
+/**
+ * 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
index 41c2fcf..3e4670e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -16,12 +16,21 @@
 
 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 positions currently visible items using [LazyMeasuredItemProvider].
+ * 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,
@@ -45,7 +54,10 @@
             firstVisibleItemIndex = DataIndex(0),
             firstVisibleItemScrollOffset = 0,
             canScrollForward = false,
-            consumedScroll = 0f
+            consumedScroll = 0f,
+            viewportStartOffset = -startContentPadding,
+            viewportEndOffset = endContentPadding,
+            totalItemsCount = 0
         )
     } else {
         var currentFirstItemIndex = firstVisibleItemIndex
@@ -97,7 +109,7 @@
             val measuredItem = itemProvider.getAndMeasure(previous)
             visibleItems.add(0, measuredItem)
             maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
-            currentFirstItemScrollOffset += measuredItem.mainAxisSize
+            currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
             currentFirstItemIndex = previous
         }
         // if we were scrolled backward, but there were not enough items before. this means
@@ -120,12 +132,12 @@
         var mainAxisUsed = -goingForwardInitialScrollOffset
         while (mainAxisUsed <= maxMainAxis && index.value < itemsCount) {
             val measuredItem = itemProvider.getAndMeasure(index)
-            mainAxisUsed += measuredItem.mainAxisSize
+            mainAxisUsed += measuredItem.sizeWithSpacings
 
             if (mainAxisUsed < minOffset) {
                 // this item is offscreen and will not be placed. advance firstVisibleItemIndex
                 currentFirstItemIndex = index + 1
-                currentFirstItemScrollOffset -= measuredItem.mainAxisSize
+                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) {
@@ -156,7 +168,7 @@
                 }
                 visibleItems.add(0, measuredItem)
                 maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
-                currentFirstItemScrollOffset += measuredItem.mainAxisSize
+                currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
                 currentFirstItemIndex = previous
             }
             scrollDelta += toScrollBack
@@ -189,7 +201,7 @@
             currentFirstItemScrollOffset += startContentPadding
             var startPaddingItems = 0
             while (startPaddingItems < visibleItems.lastIndex) {
-                val size = visibleItems[startPaddingItems].mainAxisSize
+                val size = visibleItems[startPaddingItems].sizeWithSpacings
                 if (size <= currentFirstItemScrollOffset) {
                     startPaddingItems++
                     currentFirstItemScrollOffset -= size
@@ -200,15 +212,77 @@
             }
         }
 
+        val mainAxisSize = mainAxisUsed + startContentPadding
+        val maximumVisibleOffset = minOf(mainAxisSize, mainAxisMaxSize) + endContentPadding
         return LazyListMeasureResult(
-            mainAxisSize = mainAxisUsed + startContentPadding,
+            mainAxisSize = mainAxisSize,
             crossAxisSize = maxCrossAxis,
             items = visibleItems,
             itemsScrollOffset = firstItemOffset,
             firstVisibleItemIndex = currentFirstItemIndex,
             firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
             canScrollForward = mainAxisUsed > maxOffset,
-            consumedScroll = consumedScroll
+            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
index f3a3fcd..02b13b3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
@@ -35,5 +35,10 @@
     /** 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
-)
+    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 52250e1..53b32b2 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
@@ -118,7 +118,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
@@ -127,6 +127,15 @@
     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
@@ -282,6 +291,7 @@
             canScrollForward = measureResult.canScrollForward
         )
         scrollToBeConsumed -= measureResult.consumedScroll
+        layoutInfoState.value = measureResult
         numMeasurePasses++
     }
 
@@ -353,3 +363,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
index 21dbb69..16a5ac9 100644
--- 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
@@ -26,24 +26,38 @@
  * 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 horizontalAlignment: Alignment.Horizontal?,
+    private val verticalAlignment: Alignment.Vertical?,
     private val layoutDirection: LayoutDirection,
     private val startContentPadding: Int,
-    private val endContentPadding: 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.
      */
-    val mainAxisSize: Int
+    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
@@ -51,8 +65,9 @@
             mainAxisSize += if (isVertical) it.height else it.width
             maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
         }
-        this.mainAxisSize = mainAxisSize
-        this.crossAxisSize = maxCrossAxis
+        size = mainAxisSize
+        sizeWithSpacings = size + spacing
+        crossAxisSize = maxCrossAxis
     }
 
     /**
@@ -60,18 +75,23 @@
      * 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
+        offset: Int,
+        reverseOrder: Boolean
     ) = with(scope) {
+        this@LazyMeasuredItem.offset = offset
         var mainAxisOffset = offset
-        placeables.fastForEach {
+        val indices = if (reverseOrder) placeables.lastIndex downTo 0 else placeables.indices
+        for (index in indices) {
+            val it = placeables[index]
             if (isVertical) {
-                val x =
-                    horizontalAlignment.align(it.width, layoutWidth, layoutDirection)
+                val x = requireNotNull(horizontalAlignment)
+                    .align(it.width, layoutWidth, layoutDirection)
                 if (mainAxisOffset + it.height > -startContentPadding &&
                     mainAxisOffset < layoutHeight + endContentPadding
                 ) {
@@ -79,7 +99,7 @@
                 }
                 mainAxisOffset += it.height
             } else {
-                val y = verticalAlignment.align(it.height, layoutHeight)
+                val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
                 if (mainAxisOffset + it.width > -startContentPadding &&
                     mainAxisOffset < layoutWidth + endContentPadding
                 ) {
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
index ff038ff..89ae7d7 100644
--- 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
@@ -30,7 +30,7 @@
     isVertical: Boolean,
     private val scope: SubcomposeMeasureScope,
     private val itemContentFactory: (Int) -> @Composable () -> Unit,
-    private val measuredItemFactory: (List<Placeable>) -> LazyMeasuredItem
+    private val measuredItemFactory: (DataIndex, List<Placeable>) -> LazyMeasuredItem
 ) {
     // the constraints we will measure child with. the main axis is not restricted
     private val childConstraints = Constraints(
@@ -47,6 +47,6 @@
         val placeables = scope.subcompose(index, itemContentFactory(index.value)).fastMap {
             it.measure(childConstraints)
         }
-        return measuredItemFactory(placeables)
+        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/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 5ab2ff5..e3b4f32 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
@@ -32,7 +32,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.isFocused
 import androidx.compose.ui.focusObserver
@@ -134,7 +133,6 @@
  */
 @Composable
 @OptIn(
-    ExperimentalFocus::class,
     ExperimentalTextApi::class,
     MouseTemporaryApi::class
 )
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/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 89b5fd5..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() {
 
     /**
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 96e0375..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
@@ -88,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.
  *
@@ -128,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.
  *
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 d2ed8a5..c2f7983 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
@@ -22,7 +22,7 @@
 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
@@ -436,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 a146012f..c9642e5 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,
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 e2a1373..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,7 +28,6 @@
 import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
-@OptIn(ExperimentalPointerInput::class)
 class MultitouchGestureDetectorTest(val panZoomLock: Boolean) {
     companion object {
         @JvmStatic
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 d3b5a40..1d8e90e 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
@@ -29,7 +29,6 @@
 import androidx.compose.runtime.withRunningRecomposer
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
 import androidx.compose.ui.input.pointer.ConsumedData
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
@@ -59,7 +58,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,
@@ -336,7 +334,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/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/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 c440dc1..079539b 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,28 +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.layout.TestModifierUpdater
+import androidx.compose.ui.layout.TestModifierUpdaterLayout
 import androidx.compose.ui.layout.layoutId
-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
@@ -50,7 +49,7 @@
 import org.junit.runners.model.Statement
 
 /**
- * Benchmark that sets the [LayoutNode.modifier].
+ * Benchmark that sets the LayoutNode.modifier.
  */
 @LargeTest
 @RunWith(Parameterized::class)
@@ -68,10 +67,9 @@
 
     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),
@@ -91,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
+                }
+            }
         }
     }
 
@@ -106,8 +101,8 @@
     fun setAndClearModifiers() {
         rule.activityTestRule.runOnUiThread {
             rule.benchmarkRule.measureRepeated {
-                layoutNode.modifier = combinedModifier
-                layoutNode.modifier = Modifier
+                testModifierUpdater.updateModifier(combinedModifier)
+                testModifierUpdater.updateModifier(Modifier)
             }
         }
     }
@@ -116,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)
             }
         }
     }
@@ -140,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 54eda68..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,7 +39,6 @@
 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
@@ -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,
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
index 7666c45..666cb1c 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -9,11 +9,20 @@
     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")
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
index a0461f4..d981bab 100644
--- 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
@@ -21,11 +21,15 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.lazy.LazyColumnFor
+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?) {
@@ -34,22 +38,12 @@
         val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 1000)
 
         setContent {
-            LazyColumnFor(
-                items = List(itemCount) {
-                    Entry("Item $it")
-                },
-                modifier = Modifier.fillMaxWidth(),
-                itemContent = { item ->
-                    Row {
-                        Text(text = item.contents)
-                        Spacer(modifier = Modifier.weight(1f, fill = true))
-                        Checkbox(checked = false, onCheckedChange = {})
-                    }
+            LazyColumn(modifier = Modifier.fillMaxWidth()) {
+                items(List(itemCount) { Entry("Item $it") }) {
+                    ListRow(it)
                 }
-            )
+            }
         }
-
-        reportFullyDrawn()
     }
 
     companion object {
@@ -57,4 +51,22 @@
     }
 }
 
+@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
index ca32dc3..49dd58e 100644
--- 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
@@ -28,7 +28,5 @@
         setContent {
             Text("Compose Macrobenchmark Target")
         }
-
-        reportFullyDrawn()
     }
 }
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/build.gradle b/compose/integration-tests/macrobenchmark/build.gradle
index 07444b7..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"))
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
index 82ef367..b86f8ba 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
@@ -28,4 +28,5 @@
     <queries>
         <package android:name="androidx.compose.integration.macrobenchmark.target" />
     </queries>
+    <application android:requestLegacyExternalStorage="true"/>
 </manifest>
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index a589728..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 {
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index a589728..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 {
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index a589728..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 {
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/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/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/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/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 84b16d4..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
@@ -25,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
@@ -66,7 +66,7 @@
         TriStateCheckbox(
             state = parentState,
             onClick = onParentClick,
-            colors = CheckboxConstants.defaultColors(
+            colors = CheckboxDefaults.colors(
                 checkedColor = MaterialTheme.colors.primary
             )
         )
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/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/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/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/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index 64e7730..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
@@ -275,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/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..1b45759 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,7 +36,6 @@
 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.geometry.Offset
@@ -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
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..4590ed3 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,7 +42,6 @@
 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
@@ -95,7 +94,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
 class TextFieldTest {
 
     private val ExpectedMinimumTextFieldHeight = 56.dp
@@ -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 {
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..7048b39 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
@@ -86,7 +87,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 +140,8 @@
         )
     }
 
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
     companion object {
         /**
          * The default [Saver] implementation for [BackdropScaffoldState].
@@ -177,7 +180,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 +271,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,15 +320,17 @@
                 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(
@@ -461,6 +466,13 @@
 /**
  * Contains useful constants for [BackdropScaffold].
  */
+@Deprecated(
+    "BackdropScaffoldConstants has been replaced with BackdropScaffoldDefaults",
+    ReplaceWith(
+        "BackdropScaffoldDefaults",
+        "androidx.compose.material.BackdropScaffoldDefaults"
+    )
+)
 object BackdropScaffoldConstants {
 
     /**
@@ -476,8 +488,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 +501,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/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 6b8cfc3..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
@@ -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,
@@ -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..6c502d2 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,11 +34,12 @@
 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
@@ -253,6 +254,8 @@
         )
     }
 
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
     companion object {
         /**
          * The default [Saver] implementation for [BottomDrawerState].
@@ -342,10 +345,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()) {
@@ -445,10 +448,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 +484,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()
@@ -530,6 +535,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 +549,8 @@
      */
     val DefaultElevation = 16.dp
 
-    @Composable
     val defaultScrimColor: Color
+        @Composable
         get() = MaterialTheme.colors.onSurface.copy(alpha = ScrimDefaultOpacity)
 
     /**
@@ -547,6 +559,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/ListItem.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
index eaedb99..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
@@ -111,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
@@ -166,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
@@ -185,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
@@ -267,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
@@ -283,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/MaterialTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
index 0a1c321..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
@@ -94,9 +94,9 @@
      *
      * @sample androidx.compose.material.samples.ThemeColorSample
      */
-    @Composable
-    @ComposableContract(readonly = true)
     val colors: Colors
+        @Composable
+        @ComposableContract(readonly = true)
         get() = AmbientColors.current
 
     /**
@@ -104,17 +104,17 @@
      *
      * @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
 }
 
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 f075e67..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
@@ -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
@@ -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) {
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 6e3b519..b8ed48c 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
@@ -79,7 +80,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 +132,8 @@
         )
     }
 
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
     companion object {
         /**
          * The default [Saver] implementation for [ModalBottomSheetState].
@@ -167,7 +170,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 +222,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,6 +233,7 @@
         Surface(
             Modifier
                 .fillMaxWidth()
+                .nestedScroll(sheetState.nestedScrollConnection)
                 .offset(y = { sheetState.offset.value }),
             shape = sheetShape,
             elevation = sheetElevation,
@@ -322,6 +326,13 @@
 /**
  * Contains useful constants for [ModalBottomSheetLayout].
  */
+@Deprecated(
+    "ModalBottomSheetConstants has been replaced with ModalBottomSheetDefaults",
+    ReplaceWith(
+        "ModalBottomSheetDefaults",
+        "androidx.compose.material.ModalBottomSheetDefaults"
+    )
+)
 object ModalBottomSheetConstants {
 
     /**
@@ -332,7 +343,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 6e9d2ed..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
@@ -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,
@@ -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 31d5c18..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
@@ -40,8 +40,8 @@
 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.SliderDefaults.InactiveTrackColorAlpha
+import androidx.compose.material.SliderDefaults.TickColorAlpha
 import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
@@ -59,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
@@ -196,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
@@ -208,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,
@@ -361,8 +383,8 @@
         else -> (fraction * 100).roundToInt().coerceIn(1, 99)
     }
     return semantics(mergeDescendants = true) {
-        accessibilityValue = Strings.TemplatePercent.format(percent)
-        accessibilityValueRange = AccessibilityRangeInfo(coerced, valueRange, steps)
+        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..200bd21c2 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
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 9881050..c60a39c 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
@@ -65,7 +65,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 +75,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 +117,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
@@ -208,6 +208,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 +235,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 +277,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 175501d..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
@@ -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 4486035..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
@@ -24,7 +24,7 @@
 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
@@ -69,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
@@ -96,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.
@@ -113,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
 ) {
@@ -176,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.
@@ -192,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
 ) {
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 1bacb70..e66dad5 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
@@ -39,7 +39,6 @@
 import androidx.compose.runtime.remember
 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.focusRequester
 import androidx.compose.ui.graphics.Color
@@ -73,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,
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-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 61b92204..34986e5 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 {
@@ -41,11 +41,12 @@
     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 +62,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;
@@ -97,6 +98,7 @@
     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 void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
@@ -129,7 +131,7 @@
   }
 
   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 {
@@ -168,7 +170,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);
@@ -665,6 +667,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);
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 61b92204..34986e5 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 {
@@ -41,11 +41,12 @@
     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 +62,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;
@@ -97,6 +98,7 @@
     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 void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
@@ -129,7 +131,7 @@
   }
 
   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 {
@@ -168,7 +170,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);
@@ -665,6 +667,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);
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index a649fe2..c0658ef 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 {
@@ -41,11 +41,12 @@
     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 +62,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;
@@ -97,6 +98,7 @@
     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 @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
     method @kotlin.PublishedApi internal void emitNode(Object? node);
@@ -137,7 +139,7 @@
   }
 
   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;
@@ -194,7 +196,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);
@@ -703,6 +705,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);
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 b1b6b8f..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,9 +17,9 @@
 @file:OptIn(ExperimentalComposeApi::class)
 package androidx.compose.runtime
 
+import android.view.View
 import android.widget.TextView
-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
@@ -496,17 +496,20 @@
     }
 
     @Composable fun deferredSubCompose(block: @Composable () -> Unit): () -> Unit {
-        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 4ccd97f..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,13 +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.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
@@ -116,15 +116,18 @@
 
     @Composable
     fun subCompose(block: @Composable () -> Unit) {
-        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/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 206e8f4..e39d965 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
@@ -77,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
@@ -146,6 +148,7 @@
         }
     }
 
+    @OptIn(InternalComposeApi::class)
     fun registerInsert(keyInfo: KeyInfo, insertIndex: Int) {
         groupInfos[keyInfo.location] = GroupInfo(-1, insertIndex, 0)
     }
@@ -167,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
 }
@@ -371,7 +379,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()
@@ -535,6 +543,7 @@
      * Start the composition. This should be called, and only be called, as the first group in
      * the composition.
      */
+    @OptIn(InternalComposeApi::class)
     private fun startRoot() {
         reader = slotTable.openReader()
         startGroup(rootKey)
@@ -545,6 +554,7 @@
         providersInvalidStack.push(providersInvalid.asInt())
         providersInvalid = changed(parentProvider)
         collectKeySources = parentReference.collectingKeySources
+        collectParameterInformation = parentReference.collectingParameterInformation
         resolveAmbient(InspectionTables, parentProvider)?.let {
             it.add(slotTable)
             parentReference.recordInspectionTable(it)
@@ -556,6 +566,7 @@
      * End the composition. This should be called, and only be called, to end the first group in
      * the composition.
      */
+    @OptIn(InternalComposeApi::class)
     private fun endRoot() {
         endGroup()
         parentReference.doneComposing()
@@ -569,6 +580,7 @@
     /**
      * Discard a pending composition because an error was encountered during composition
      */
+    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     private fun abortRoot() {
         cleanUpCompose()
         pendingStack.clear()
@@ -621,6 +633,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.
      *
@@ -756,6 +777,7 @@
      * Apply the changes to the tree that were collected during the last composition.
      */
     @InternalComposeApi
+    @OptIn(ExperimentalComposeApi::class)
     fun applyChanges() {
         trace("Compose:applyChanges") {
             invalidateStack.clear()
@@ -804,6 +826,7 @@
     }
 
     @ExperimentalComposeApi
+    @OptIn(InternalComposeApi::class)
     internal fun dispose() {
         trace("Compose:Composer.dispose") {
             parentReference.unregisterComposer(this)
@@ -845,6 +868,7 @@
      */
     internal fun endGroup() = end(false)
 
+    @OptIn(InternalComposeApi::class)
     private fun skipGroup() {
         groupNodeCount += reader.skipGroup()
     }
@@ -867,6 +891,7 @@
      * call when the composer is inserting.
      */
     @Suppress("UNUSED")
+    @OptIn(ExperimentalComposeApi::class)
     internal fun <T : N> createNode(factory: () -> T) {
         validateNodeExpected()
         check(inserting) { "createNode() can only be called when inserting" }
@@ -876,9 +901,14 @@
         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)
+        }
     }
 
     /**
@@ -886,6 +916,7 @@
      * inserting.
      */
     @PublishedApi
+    @OptIn(ExperimentalComposeApi::class)
     internal fun emitNode(node: Any?) {
         validateNodeExpected()
         check(inserting) { "emitNode() called when not inserting" }
@@ -895,9 +926,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)
+        }
     }
 
     /**
@@ -906,6 +942,7 @@
      * location as [emitNode] or [createNode] as called even if the value is unused.
      */
     @PublishedApi
+    @OptIn(InternalComposeApi::class)
     internal fun useNode(): N {
         validateNodeExpected()
         check(!inserting) { "useNode() called while inserting" }
@@ -925,6 +962,7 @@
      * node that is the current node in the tree which was either created by [createNode],
      * emitted by [emitNode] or returned by [useNode].
      */
+    @OptIn(ExperimentalComposeApi::class)
     internal fun <V, T> apply(value: V, block: T.(V) -> Unit) {
         recordApplierOperation { applier, _, _ ->
             @Suppress("UNCHECKED_CAST")
@@ -937,6 +975,7 @@
      * use the key stored at the current location in the slot table to avoid allocating a new key.
      */
     @ComposeCompilerApi
+    @OptIn(InternalComposeApi::class)
     fun joinKey(left: Any?, right: Any?): Any =
         getKey(reader.groupObjectKey, left, right) ?: JoinedKey(left, right)
 
@@ -944,6 +983,7 @@
      * Return the next value in the slot table and advance the current location.
      */
     @PublishedApi
+    @OptIn(InternalComposeApi::class)
     internal fun nextSlot(): Any? = if (inserting) {
         validateNodeNotExpected()
         EMPTY
@@ -1081,6 +1121,7 @@
      * @param value the value to schedule to be written to the slot table.
      */
     @PublishedApi
+    @OptIn(InternalComposeApi::class)
     internal fun updateValue(value: Any?) {
         if (inserting) {
             writer.update(value)
@@ -1256,11 +1297,16 @@
         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)
         }
@@ -1567,7 +1613,7 @@
         val inserting = inserting
         if (inserting) {
             if (isNode) {
-                recordInsertUp()
+                registerInsertUp()
                 expectedNodeCount = 1
             }
             reader.endEmpty()
@@ -1588,8 +1634,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) {
@@ -1967,7 +2013,7 @@
         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)
@@ -2038,11 +2084,14 @@
     internal fun hasInvalidations() = invalidations.isNotEmpty()
 
     @Suppress("UNCHECKED_CAST")
+    @OptIn(InternalComposeApi::class)
     private var SlotWriter.node
         get() = node(currentGroup) as N
         set(value) { updateParentNode(value) }
+
     @Suppress("UNCHECKED_CAST")
     private val SlotReader.node get() = node(parent) as N
+
     @Suppress("UNCHECKED_CAST")
     private fun SlotReader.nodeAt(index: Int) = node(index) as N
 
@@ -2145,20 +2194,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())
     }
 
     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.
     //
@@ -2384,7 +2445,8 @@
     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
         val composers = mutableSetOf<Composer<*>>()
@@ -2428,7 +2490,17 @@
         }
 
         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)
         }
 
         override fun <T> getAmbient(key: Ambient<T>): T {
@@ -2618,7 +2690,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.
@@ -2798,8 +2870,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")
 }
 
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..26940dd 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,6 +36,7 @@
 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<*>)
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/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index b543735..03bdc3d 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,6 +433,10 @@
     internal override val collectingKeySources: Boolean
         get() = false
 
+    // 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<SlotTable>) {
         // 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..b1f49c7 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
@@ -773,7 +773,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
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/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/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 1374742b..fde53a01 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
@@ -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(SlotTable(), 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(SlotTable(), 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(SlotTable(), 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(SlotTable(), 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) {
@@ -2664,3 +2791,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/SlotTableTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt
index 2f61e12..5910f88 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,6 +3047,26 @@
             }
         }
     }
+
+    @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)
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 cdc65bb..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
@@ -308,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-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 2b56325..b18ab3f 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);
@@ -430,7 +438,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 +453,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 +467,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 +522,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;
@@ -612,6 +634,12 @@
     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;
@@ -860,14 +888,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);
@@ -906,14 +934,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);
@@ -982,23 +1010,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 2b56325..b18ab3f 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);
@@ -430,7 +438,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 +453,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 +467,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 +522,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;
@@ -612,6 +634,12 @@
     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;
@@ -860,14 +888,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);
@@ -906,14 +934,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);
@@ -982,23 +1010,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 f6b1064..acc2da8 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);
@@ -462,7 +470,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 +485,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 +499,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 +554,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;
@@ -644,6 +666,12 @@
     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;
@@ -892,14 +920,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);
@@ -962,14 +990,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);
@@ -1038,23 +1066,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/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/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/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/drawscope/CanvasDrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
index a73b4a4..ef1f005 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
                     }
         }
 
@@ -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,7 +634,7 @@
         miter: Float,
         cap: StrokeCap,
         join: StrokeJoin,
-        pathEffect: NativePathEffect?,
+        pathEffect: PathEffect?,
         @FloatRange(from = 0.0, to = 1.0) alpha: Float,
         colorFilter: ColorFilter?,
         blendMode: BlendMode
@@ -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/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/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-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 e6bda1c2..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -7,7 +7,7 @@
 
   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;
@@ -96,5 +100,8 @@
     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 e6bda1c2..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
@@ -7,7 +7,7 @@
 
   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;
@@ -96,5 +100,8 @@
     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 e6bda1c2..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -7,7 +7,7 @@
 
   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;
@@ -96,5 +100,8 @@
     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 2c038c3..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,7 +20,7 @@
 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
@@ -44,8 +44,8 @@
 
     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))
             }
         }
@@ -67,7 +67,7 @@
         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))
@@ -84,7 +84,7 @@
         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))
@@ -107,7 +107,7 @@
         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()
@@ -126,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 0adcd1b..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
@@ -114,7 +114,7 @@
     }
 
     /**
-     * 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
@@ -138,28 +138,28 @@
 
         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 snapshot is applied
-            wasIdleBeforeApplySnapshot = composeIdlingResource.isIdle()
+            wasIdleBeforeApplySnapshot = composeIdlingResource.isIdleNow
 
             // Apply the snapshot
             @OptIn(ExperimentalComposeApi::class)
             Snapshot.sendApplyNotifications()
 
             // Record idleness after this snapshot is applied
-            wasIdleAfterApplySnapshot = composeIdlingResource.isIdle()
+            wasIdleAfterApplySnapshot = composeIdlingResource.isIdleNow
 
             // Record idleness after the first recomposition
             @OptIn(ExperimentalCoroutinesApi::class)
             scope.async(start = CoroutineStart.UNDISPATCHED) {
                 // Await a single recomposition
                 withFrameNanos {}
-                composeIdlingResource.isIdle()
+                composeIdlingResource.isIdleNow
             }
         }.let {
             runBlocking {
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 2e199ea..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
@@ -18,10 +18,8 @@
 
 import androidx.activity.ComponentActivity
 import androidx.compose.testutils.expectError
-import androidx.compose.ui.platform.AndroidOwner
+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
@@ -51,7 +49,7 @@
 
     @Before
     fun addMockResumedOwner() {
-        androidOwnerRegistry.registerOwner(mockResumedAndroidOwner())
+        androidOwnerRegistry.registerOwner(mockResumedRootForTest())
     }
 
     @Test
@@ -119,21 +117,9 @@
         }
     }
 
-    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 a6fd566..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
@@ -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/AndroidComposeTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
index 7fcac78..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
@@ -23,12 +23,14 @@
 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.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
@@ -138,8 +140,13 @@
         activityProvider: (R) -> A
     ) : this(activityRule, activityProvider, false)
 
+    private val idlingResourceRegistry = IdlingResourceRegistry()
+    private val espressoLink = EspressoLink(idlingResourceRegistry)
+
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    internal val composeIdlingResource = ComposeIdlingResource()
+    internal val composeIdlingResource = ComposeIdlingResource().also {
+        registerIdlingResource(it)
+    }
 
     @ExperimentalTesting
     override val clockTestRule: AnimationClockTestRule =
@@ -174,11 +181,13 @@
         }
     }
 
-    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 { base, _ -> composeIdlingResource.getStatementFor(base) }
+            .around { base, _ -> idlingResourceRegistry.getStatementFor(base) }
+            .around { base, _ -> espressoLink.getStatementFor(base) }
             .around(clockTestRule)
             .around { base, _ -> AndroidComposeStatement(base) }
             .around(activityRule)
@@ -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() {
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 b7ad00d..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
@@ -18,7 +18,7 @@
 
 import android.annotation.SuppressLint
 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
@@ -33,7 +33,7 @@
 
     @SuppressLint("DocumentExceptions")
     override fun sendTextInputCommand(node: SemanticsNode, command: List<EditOperation>) {
-        val owner = node.layoutNode.owner as AndroidOwner
+        val owner = node.owner as ViewRootForTest
 
         @Suppress("DEPRECATION")
         runOnUiThread {
@@ -46,7 +46,7 @@
 
     @SuppressLint("DocumentExceptions")
     override fun sendImeAction(node: SemanticsNode, actionSpecified: ImeAction) {
-        val owner = node.layoutNode.owner as AndroidOwner
+        val owner = node.owner as ViewRootForTest
 
         @Suppress("DEPRECATION")
         runOnUiThread {
@@ -66,7 +66,7 @@
         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/android/AndroidOwnerRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
index 0138a73..32fdccc 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
@@ -18,9 +18,8 @@
 
 import android.view.View
 import androidx.annotation.VisibleForTesting
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.test.ExperimentalTesting
-import androidx.lifecycle.Lifecycle
 import kotlinx.coroutines.suspendCancellableCoroutine
 import org.junit.runners.model.Statement
 import java.util.Collections
@@ -32,24 +31,25 @@
 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 class AndroidOwnerRegistry {
-    private val owners = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
+    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
      */
     private fun setupRegistry() {
-        AndroidOwner.onAndroidOwnerCreatedCallback = ::onAndroidOwnerCreated
+        ViewRootForTest.onViewCreatedCallback = ::onComposeViewCreated
     }
 
     /**
@@ -57,9 +57,9 @@
      */
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal fun tearDownRegistry() {
-        AndroidOwner.onAndroidOwnerCreatedCallback = null
+        ViewRootForTest.onViewCreatedCallback = null
         synchronized(owners) {
-            getUnfilteredOwners().forEach {
+            owners.toList().forEach {
                 unregisterOwner(it)
             }
             // Remove all listeners as well, now we've unregistered all owners
@@ -67,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> {
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal 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.
-     * This method is almost always preferred over [getUnfilteredOwners].
+     * Returns a copy of the set of all registered [ViewRootForTest]s that can be interacted with.
      */
-    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) }
@@ -106,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)
         }
@@ -115,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)
@@ -126,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)
@@ -148,15 +147,15 @@
     }
 
     /**
-     * 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 inner class OwnerAttachedListener(
-        private val owner: AndroidOwner
+        private val owner: ViewRootForTest
     ) : View.OnAttachStateChangeListener {
 
         // Note: owner.view === view, because the owner _is_ the view,
@@ -187,7 +186,7 @@
     if (!hasAndroidOwners) {
         val latch = CountDownLatch(1)
         val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-            override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+            override fun onRegistrationChanged(owner: ViewRootForTest, registered: Boolean) {
                 if (hasAndroidOwners) {
                     latch.countDown()
                 }
@@ -220,9 +219,12 @@
                 }
             }
 
-            // Usually we resume if an AndroidOwner is registered while the listener is added
+            // Usually we resume if an ComposeViewTestMarker is registered while the listener is added
             val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-                override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+                override fun onRegistrationChanged(
+                    owner: ViewRootForTest,
+                    registered: Boolean
+                ) {
                     if (hasAndroidOwners) {
                         resume(this)
                     }
@@ -234,7 +236,7 @@
                 removeOnRegistrationChangedListener(listener)
             }
 
-            // Sometimes the AndroidOwner was registered before we added
+            // 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 ee8879a..d380d2f 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,36 +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.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.TestAnimationClock
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.junit4.isOnUiThread
-import androidx.compose.ui.test.junit4.runOnUiThread
-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 kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import org.junit.runners.model.Statement
-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?
-}
 
 /**
  * Register compose's idling check to Espresso.
@@ -119,12 +102,7 @@
  * resource is automatically registered when any compose testing APIs are used including
  * [createAndroidComposeRule].
  */
-internal class ComposeIdlingResource : IdlingResource, IdlingResourceWithDiagnostics {
-
-    override fun getName(): String = "ComposeIdlingResource"
-
-    private var isIdleCheckScheduled = false
-    private var resourceCallback: IdlingResource.ResourceCallback? = null
+internal class ComposeIdlingResource : IdlingResource {
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal val androidOwnerRegistry = AndroidOwnerRegistry()
@@ -132,103 +110,34 @@
     @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 hadNoPendingMeasureLayout = true
-    // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
-//    private var hadNoPendingDraw = true
-
-    private var compositionAwaiters = AtomicInteger(0)
+    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()
+            val owners = androidOwnerRegistry.getOwners()
             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)"
+            hadNoPendingDraw = !owners.any {
+                val hasContent = it.view.measuredWidth != 0 && it.view.measuredHeight != 0
+                it.view.isDirty && (hasContent || it.view.isLayoutRequested)
             }
 
-            hadNoSnapshotChanges &&
+            return hadNoSnapshotChanges &&
                 hadNoRecomposerChanges &&
                 hadAnimationClocksIdle &&
-                lastCompositionAwaiters == 0 &&
-                // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
-                hadNoPendingMeasureLayout /*&&
-                hadNoPendingDraw*/
+                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
-            )
-        }
-    }
-
-    private fun transitionToIdle() {
-        resourceCallback?.onTransitionToIdle()
-    }
-
-    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
-        resourceCallback = callback
-    }
-
-    /**
-     * 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)
     fun registerTestClock(clock: TestAnimationClock) {
@@ -255,14 +164,10 @@
         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 hadPendingDraw = !hadNoPendingDraw
 
-        val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges &&
-            !hadRunningAnimations && !wasAwaitingCompositions
+        val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges && !hadRunningAnimations
 
         if (wasIdle) {
             return null
@@ -272,21 +177,19 @@
         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"
+            message += "hadPendingMeasureLayout = $hadPendingMeasureLayout, "
+            message += "hadPendingDraw = $hadPendingDraw"
         }
         return message
     }
@@ -341,101 +244,6 @@
     }
 
     fun getStatementFor(base: Statement): Statement {
-        return androidOwnerRegistry.getStatementFor(
-            object : Statement() {
-                override fun evaluate() {
-                    try {
-                        IdlingRegistry.getInstance().register(this@ComposeIdlingResource)
-                        base.evaluate()
-                    } finally {
-                        IdlingRegistry.getInstance().unregister(this@ComposeIdlingResource)
-                    }
-                }
-            }
-        )
+        return androidOwnerRegistry.getStatementFor(base)
     }
 }
-
-// TODO(b/168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
-//  and blocking await Espresso.onIdle().
-internal fun ComposeIdlingResource.runEspressoOnIdle() {
-    val compositionAwaiter = CompositionAwaiter(this)
-    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
-    }
-}
-
-private 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
-}
-
-/**
- * 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/CompositionAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
deleted file mode 100644
index dd380e9..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 val composeIdlingResource: ComposeIdlingResource) {
-
-    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/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 282ac47..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,6 +26,7 @@
 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
@@ -130,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/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 203fed5b..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,8 +19,8 @@
 import androidx.test.espresso.matcher.ViewMatchers
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.semantics.SemanticsNode
 
 internal actual fun SemanticsNodeInteraction.checkIsDisplayed(): Boolean {
@@ -28,16 +28,16 @@
     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.layoutNode
-    if (isNotPlaced(layoutNode) || layoutNode.findClosestParentNode(::isNotPlaced) != null) {
+    val layoutInfo = node.layoutInfo
+    if (isNotPlaced(layoutInfo) || layoutInfo.findClosestParentNode(::isNotPlaced) != null) {
         return false
     }
 
-    (node.layoutNode.owner as? AndroidOwner)?.let {
+    (node.owner as? ViewRootForTest)?.let {
         if (!ViewMatchers.isDisplayed().matches(it.view)) {
             return false
         }
@@ -53,7 +53,7 @@
 }
 
 internal actual fun SemanticsNode.clippedNodeBoundsInWindow(): Rect {
-    val composeView = (layoutNode.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())
@@ -62,7 +62,7 @@
 }
 
 internal actual fun SemanticsNode.isInScreenBounds(): Boolean {
-    val composeView = (layoutNode.owner as AndroidOwner).view
+    val composeView = (owner as ViewRootForTest).view
 
     // Window relative bounds of our node
     val nodeBoundsInWindow = clippedNodeBoundsInWindow()
@@ -83,17 +83,19 @@
 }
 
 /**
- * Executes [selector] on every parent of this [LayoutNode] and returns the closest
- * [LayoutNode] to return `true` from [selector] or null if [selector] returns false
+ * 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 LayoutNode.findClosestParentNode(selector: (LayoutNode) -> Boolean): LayoutNode? {
-    var currentParent = this.parent
+private fun LayoutInfo.findClosestParentNode(
+    selector: (LayoutInfo) -> Boolean
+): LayoutInfo? {
+    var currentParent = this.parentInfo
     while (currentParent != null) {
         if (selector(currentParent)) {
             return currentParent
         } else {
-            currentParent = currentParent.parent
+            currentParent = currentParent.parentInfo
         }
     }
 
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 eaf81f0..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,7 +21,7 @@
 import android.view.Window
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.graphics.ImageBitmap
-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
@@ -53,7 +53,7 @@
         )
     }
 
-    val view = (node.layoutNode.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 f94df4a..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 createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
-    require(owner is AndroidOwner) {
-        "InputDispatcher currently only supports dispatching to AndroidOwner, not to " +
+    require(owner is ViewRootForTest) {
+        "InputDispatcher currently only supports dispatching to ViewRootForTest, not to " +
             owner::class.java.simpleName
     }
     val view = owner.view
@@ -45,7 +45,7 @@
 
 internal class AndroidInputDispatcher(
     testContext: TestContext,
-    owner: AndroidOwner?,
+    owner: Owner?,
     private val sendEvent: (MotionEvent) -> Unit
 ) : InputDispatcher(testContext, owner) {
 
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 de53bce..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
@@ -64,8 +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.
-    val viewPortInParent = scrollableNode.layoutNode.coordinates.boundsInParent
-    val parentInRoot = scrollableNode.layoutNode.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 d4b7af9..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
@@ -106,7 +106,7 @@
         }
 
     // Convenience property
-    private val owner get() = semanticsNode.layoutNode.owner
+    private val owner get() = semanticsNode.owner
 
     // TODO(b/133217292): Better error: explain which gesture couldn't be performed
     private var _inputDispatcher: InputDispatcher? =
@@ -285,7 +285,7 @@
  * @param position A position in local coordinates
  */
 private fun GestureScope.localToGlobal(position: Offset): Offset {
-    return position + semanticsNode.layoutNode.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/KeyInputHelpers.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
index 3892185..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,7 +16,6 @@
 
 package androidx.compose.ui.test
 
-import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.KeyEvent
 
 /**
@@ -24,10 +23,9 @@
  *
  * @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})")
-    val owner = semanticsNode.layoutNode.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/SemanticsNodeInteraction.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
index 7b55269..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
@@ -248,7 +248,7 @@
     operation: Density.(SemanticsNode) -> R
 ): R {
     val node = fetchSemanticsNode("Failed to retrieve density for the node.")
-    val density = node.layoutNode.owner!!.density
+    val density = node.owner!!.density
     return operation.invoke(density, node)
 }
 
@@ -256,7 +256,7 @@
     assertion: Density.(Rect) -> Unit
 ): SemanticsNodeInteraction {
     val node = fetchSemanticsNode("Failed to retrieve bounds of the node.")
-    val density = node.layoutNode.owner!!.density
+    val density = node.owner!!.density
 
     assertion.invoke(density, node.unclippedBoundsInRoot)
     return this
diff --git a/compose/ui/ui-tooling/api/current.txt b/compose/ui/ui-tooling/api/current.txt
index 23de9c4..ca0c914 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;
   }
 
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..ca0c914 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;
   }
 
diff --git a/compose/ui/ui-tooling/api/restricted_current.txt b/compose/ui/ui-tooling/api/restricted_current.txt
index 23de9c4..ca0c914 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;
   }
 
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..d3d094f 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
@@ -26,7 +26,7 @@
 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
@@ -81,9 +81,9 @@
             WeakHashMap<SlotTable, 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/LayoutInspectorTreeTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 73d6452..4ecf4eb 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,6 +34,8 @@
 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
@@ -461,6 +464,30 @@
         assertThat(node).isNotNull()
     }
 
+    @Test // regression test b/174855322
+    fun testBasicText() {
+        val slotTableRecord = SlotTableRecord.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")
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..1f1b9d2 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
@@ -59,6 +59,7 @@
     content: @Composable () -> Unit
 ) {
     currentComposer.collectKeySourceInformation()
+    currentComposer.collectParameterInformation()
     val store = (slotTableRecord as SlotTableRecordImpl).store
     store.add(currentComposer.slotTable)
     Providers(
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 e93ccb9..265bcfc 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
@@ -23,8 +23,8 @@
 import androidx.compose.runtime.SlotTable
 import androidx.compose.runtime.keySourceInfoOf
 import androidx.compose.ui.layout.globalPosition
-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
@@ -455,7 +455,7 @@
         children.add(getGroup(context))
     }
 
-    val modifierInfo = if (node is LayoutNode) {
+    val modifierInfo = if (node is LayoutInfo) {
         node.getModifierInfo()
     } else {
         emptyList()
@@ -463,7 +463,7 @@
 
     // 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) }
@@ -491,8 +491,8 @@
         )
 }
 
-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,
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 5f5bcaf..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,7 +16,7 @@
 
 package androidx.compose.ui.tooling.inspector
 
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.layout.LayoutInfo
 
 /**
  * Node representing a Composable for the Layout Inspector.
@@ -106,7 +106,7 @@
  */
 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 6f30139..b831653 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
@@ -19,7 +19,7 @@
 import android.view.View
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.SlotTable
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.layout.LayoutInfo
 import androidx.compose.ui.node.OwnedLayer
 import androidx.compose.ui.tooling.Group
 import androidx.compose.ui.tooling.NodeGroup
@@ -62,8 +62,8 @@
     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 */
@@ -121,20 +121,20 @@
     }
 
     /**
-     * Stitch separate trees together using the [LayoutNode]s found in the [SlotTable]s.
+     * Stitch separate trees together using the [LayoutInfo]s found in the [SlotTable]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].
+     * at the parent of each [LayoutInfo].
      * 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
             }
@@ -262,7 +262,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/preview/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
index 7e6d2b6..e25b4ae 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,9 +40,9 @@
 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
@@ -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..efa8bdd 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);
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..efa8bdd 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);
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index 8f93c6c..efa8bdd 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);
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 c05fc73..9f00a14c 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -132,26 +132,29 @@
     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);
   }
 
-  public final class FocusModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+  public final class FocusModifierKt {
+    method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+  }
+
+  public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
     method 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 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 {
@@ -194,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();
@@ -216,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);
@@ -224,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;
@@ -328,27 +331,49 @@
 
 package androidx.compose.ui.focus {
 
-  @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
-  }
-
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+  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;
@@ -410,9 +435,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);
   }
@@ -493,11 +515,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);
@@ -505,7 +527,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;
@@ -517,6 +539,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 {
@@ -524,7 +577,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);
@@ -678,7 +731,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);
@@ -811,7 +865,7 @@
 
 package androidx.compose.ui.input.key {
 
-  @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
+  @Deprecated public interface Alt {
     method @Deprecated public boolean isLeftAltPressed();
     method @Deprecated public default boolean isPressed();
     method @Deprecated public boolean isRightAltPressed();
@@ -820,9 +874,6 @@
     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);
@@ -1416,7 +1467,7 @@
     property public final int ZoomOut;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
+  public interface KeyEvent {
     method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
     method public int getKey-EK5gGoQ();
     method public androidx.compose.ui.input.key.KeyEventType getType();
@@ -1435,15 +1486,15 @@
     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 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 previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
   }
 
 }
@@ -1472,8 +1523,7 @@
     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);
+  @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope 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();
@@ -1600,12 +1650,10 @@
     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 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;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1626,7 +1674,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);
   }
 
 }
@@ -1739,6 +1787,22 @@
     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);
@@ -1794,6 +1858,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);
   }
@@ -1899,6 +1973,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);
   }
@@ -1923,25 +2000,15 @@
   @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 {
   }
 
-  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();
+  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 @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.unit.LayoutDirection getLayoutDirection();
-    method public androidx.compose.ui.node.MeasureBlocks getMeasureBlocks();
-    method public androidx.compose.ui.Modifier getModifier();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
-    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 boolean isAttached();
     method public boolean isPlaced();
     method public boolean isValid();
     method public int maxIntrinsicHeight(int width);
@@ -1949,28 +2016,14 @@
     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 place(int x, int y);
-    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 setLayoutDirection(androidx.compose.ui.unit.LayoutDirection value);
-    method public void setMeasureBlocks(androidx.compose.ui.node.MeasureBlocks value);
-    method public void setModifier(androidx.compose.ui.Modifier value);
-    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.unit.LayoutDirection layoutDirection;
-    property public final androidx.compose.ui.node.MeasureBlocks measureBlocks;
-    property public final androidx.compose.ui.Modifier modifier;
-    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 final class LayoutNodeKt {
@@ -1984,16 +2037,6 @@
     method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
   }
 
-  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);
@@ -2017,7 +2060,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();
@@ -2035,7 +2077,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;
@@ -2043,7 +2085,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;
@@ -2079,16 +2120,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 {
@@ -2126,8 +2164,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();
@@ -2159,31 +2195,6 @@
   public final class AndroidComposeViewKt {
   }
 
-  public interface AndroidOwner extends androidx.compose.ui.node.Owner {
-    method 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);
@@ -2262,10 +2273,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);
   }
@@ -2317,6 +2324,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;
@@ -2412,7 +2436,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();
@@ -2462,9 +2486,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);
   }
@@ -2612,8 +2638,9 @@
     method public androidx.compose.ui.geometry.Rect getGlobalBounds();
     method public long getGlobalPosition-F1C5BW0();
     method public int getId();
-    method public androidx.compose.ui.node.LayoutNode getLayoutNode();
+    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();
@@ -2625,8 +2652,9 @@
     property public final long globalPosition;
     property public final int id;
     property public final boolean isRoot;
-    property public final androidx.compose.ui.node.LayoutNode layoutNode;
+    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;
@@ -2649,9 +2677,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();
@@ -2660,14 +2687,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;
@@ -2676,6 +2703,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;
@@ -2690,14 +2718,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);
@@ -2710,9 +2740,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);
@@ -2720,6 +2750,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);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index c05fc73..9f00a14c 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -132,26 +132,29 @@
     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);
   }
 
-  public final class FocusModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+  public final class FocusModifierKt {
+    method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+  }
+
+  public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
     method 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 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 {
@@ -194,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();
@@ -216,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);
@@ -224,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;
@@ -328,27 +331,49 @@
 
 package androidx.compose.ui.focus {
 
-  @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
-  }
-
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+  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;
@@ -410,9 +435,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);
   }
@@ -493,11 +515,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);
@@ -505,7 +527,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;
@@ -517,6 +539,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 {
@@ -524,7 +577,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);
@@ -678,7 +731,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);
@@ -811,7 +865,7 @@
 
 package androidx.compose.ui.input.key {
 
-  @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
+  @Deprecated public interface Alt {
     method @Deprecated public boolean isLeftAltPressed();
     method @Deprecated public default boolean isPressed();
     method @Deprecated public boolean isRightAltPressed();
@@ -820,9 +874,6 @@
     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);
@@ -1416,7 +1467,7 @@
     property public final int ZoomOut;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
+  public interface KeyEvent {
     method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
     method public int getKey-EK5gGoQ();
     method public androidx.compose.ui.input.key.KeyEventType getType();
@@ -1435,15 +1486,15 @@
     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 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 previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
   }
 
 }
@@ -1472,8 +1523,7 @@
     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);
+  @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope 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();
@@ -1600,12 +1650,10 @@
     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 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;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1626,7 +1674,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);
   }
 
 }
@@ -1739,6 +1787,22 @@
     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);
@@ -1794,6 +1858,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);
   }
@@ -1899,6 +1973,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);
   }
@@ -1923,25 +2000,15 @@
   @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 {
   }
 
-  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();
+  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 @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.unit.LayoutDirection getLayoutDirection();
-    method public androidx.compose.ui.node.MeasureBlocks getMeasureBlocks();
-    method public androidx.compose.ui.Modifier getModifier();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
-    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 boolean isAttached();
     method public boolean isPlaced();
     method public boolean isValid();
     method public int maxIntrinsicHeight(int width);
@@ -1949,28 +2016,14 @@
     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 place(int x, int y);
-    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 setLayoutDirection(androidx.compose.ui.unit.LayoutDirection value);
-    method public void setMeasureBlocks(androidx.compose.ui.node.MeasureBlocks value);
-    method public void setModifier(androidx.compose.ui.Modifier value);
-    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.unit.LayoutDirection layoutDirection;
-    property public final androidx.compose.ui.node.MeasureBlocks measureBlocks;
-    property public final androidx.compose.ui.Modifier modifier;
-    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 final class LayoutNodeKt {
@@ -1984,16 +2037,6 @@
     method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
   }
 
-  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);
@@ -2017,7 +2060,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();
@@ -2035,7 +2077,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;
@@ -2043,7 +2085,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;
@@ -2079,16 +2120,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 {
@@ -2126,8 +2164,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();
@@ -2159,31 +2195,6 @@
   public final class AndroidComposeViewKt {
   }
 
-  public interface AndroidOwner extends androidx.compose.ui.node.Owner {
-    method 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);
@@ -2262,10 +2273,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);
   }
@@ -2317,6 +2324,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;
@@ -2412,7 +2436,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();
@@ -2462,9 +2486,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);
   }
@@ -2612,8 +2638,9 @@
     method public androidx.compose.ui.geometry.Rect getGlobalBounds();
     method public long getGlobalPosition-F1C5BW0();
     method public int getId();
-    method public androidx.compose.ui.node.LayoutNode getLayoutNode();
+    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();
@@ -2625,8 +2652,9 @@
     property public final long globalPosition;
     property public final int id;
     property public final boolean isRoot;
-    property public final androidx.compose.ui.node.LayoutNode layoutNode;
+    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;
@@ -2649,9 +2677,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();
@@ -2660,14 +2687,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;
@@ -2676,6 +2703,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;
@@ -2690,14 +2718,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);
@@ -2710,9 +2740,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);
@@ -2720,6 +2750,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);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 5461ac24..1cffa60 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -132,26 +132,29 @@
     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);
   }
 
-  public final class FocusModifierKt {
-    method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
   }
 
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+  public final class FocusModifierKt {
+    method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+  }
+
+  public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
     method 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 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 {
@@ -194,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();
@@ -216,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);
@@ -224,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;
@@ -328,27 +331,49 @@
 
 package androidx.compose.ui.focus {
 
-  @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
-  }
-
-  @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+  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;
@@ -410,9 +435,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);
   }
@@ -493,11 +515,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);
@@ -505,7 +527,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;
@@ -517,6 +539,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 {
@@ -524,7 +577,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);
@@ -678,7 +731,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);
@@ -811,7 +865,7 @@
 
 package androidx.compose.ui.input.key {
 
-  @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
+  @Deprecated public interface Alt {
     method @Deprecated public boolean isLeftAltPressed();
     method @Deprecated public default boolean isPressed();
     method @Deprecated public boolean isRightAltPressed();
@@ -820,9 +874,6 @@
     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);
@@ -1416,7 +1467,7 @@
     property public final int ZoomOut;
   }
 
-  @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
+  public interface KeyEvent {
     method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
     method public int getKey-EK5gGoQ();
     method public androidx.compose.ui.input.key.KeyEventType getType();
@@ -1435,15 +1486,15 @@
     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 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 previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
   }
 
 }
@@ -1472,8 +1523,7 @@
     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);
+  @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope 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();
@@ -1600,12 +1650,10 @@
     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 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;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1626,7 +1674,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);
   }
 
 }
@@ -1785,6 +1833,22 @@
     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);
@@ -1841,6 +1905,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);
   }
@@ -1946,6 +2020,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);
   }
@@ -1985,25 +2062,15 @@
     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;
   }
 
-  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();
+  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 @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.unit.LayoutDirection getLayoutDirection();
-    method public androidx.compose.ui.node.MeasureBlocks getMeasureBlocks();
-    method public androidx.compose.ui.Modifier getModifier();
-    method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
-    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 boolean isAttached();
     method public boolean isPlaced();
     method public boolean isValid();
     method public int maxIntrinsicHeight(int width);
@@ -2011,28 +2078,14 @@
     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 place(int x, int y);
-    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 setLayoutDirection(androidx.compose.ui.unit.LayoutDirection value);
-    method public void setMeasureBlocks(androidx.compose.ui.node.MeasureBlocks value);
-    method public void setModifier(androidx.compose.ui.Modifier value);
-    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.unit.LayoutDirection layoutDirection;
-    property public final androidx.compose.ui.node.MeasureBlocks measureBlocks;
-    property public final androidx.compose.ui.Modifier modifier;
-    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 final class LayoutNodeKt {
@@ -2046,16 +2099,6 @@
     method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
   }
 
-  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);
@@ -2079,7 +2122,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();
@@ -2097,7 +2139,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;
@@ -2105,7 +2147,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;
@@ -2141,16 +2182,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 {
@@ -2188,8 +2226,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();
@@ -2221,31 +2257,6 @@
   public final class AndroidComposeViewKt {
   }
 
-  public interface AndroidOwner extends androidx.compose.ui.node.Owner {
-    method 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);
@@ -2324,10 +2335,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);
   }
@@ -2379,6 +2386,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;
@@ -2474,7 +2498,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();
@@ -2524,9 +2548,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);
   }
@@ -2674,8 +2700,9 @@
     method public androidx.compose.ui.geometry.Rect getGlobalBounds();
     method public long getGlobalPosition-F1C5BW0();
     method public int getId();
-    method public androidx.compose.ui.node.LayoutNode getLayoutNode();
+    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();
@@ -2687,8 +2714,9 @@
     property public final long globalPosition;
     property public final int id;
     property public final boolean isRoot;
-    property public final androidx.compose.ui.node.LayoutNode layoutNode;
+    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;
@@ -2711,9 +2739,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();
@@ -2722,14 +2749,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;
@@ -2738,6 +2765,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;
@@ -2752,14 +2780,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);
@@ -2772,9 +2802,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);
@@ -2782,6 +2812,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);
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/autofill/ExplicitAutofillTypesDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
index 9a32990..930bf45 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,10 +29,10 @@
 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.geometry.Offset
@@ -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") }
@@ -112,6 +109,7 @@
     }
 }
 
+@ExperimentalComposeUiApi
 @Composable
 private fun Autofill(
     autofillTypes: List<AutofillType>,
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..fd42463 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,7 +28,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.focus.isFocused
 import androidx.compose.ui.focusObserver
@@ -58,7 +57,6 @@
 }
 
 @Composable
-@OptIn(ExperimentalFocus::class)
 private fun FocusableText(text: String) {
     var color by remember { mutableStateOf(Black) }
     val focusRequester = FocusRequester()
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..f3face6 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,7 +30,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.focus.isFocused
 import androidx.compose.ui.focusObserver
@@ -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)) {
@@ -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)) {
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/keyinput/KeyInputDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt
index 4fe780d..9555fea 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,14 +29,12 @@
 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.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
 
@@ -63,10 +61,6 @@
 }
 
 @Composable
-@OptIn(
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class
-)
 private fun FocusableText(text: MutableState<String>) {
     var color by remember { mutableStateOf(Color.Black) }
     val focusRequester = FocusRequester()
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/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 40ac0f4..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
@@ -58,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
@@ -174,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 480bae1..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
@@ -34,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
@@ -50,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
@@ -111,9 +110,9 @@
         val info = AccessibilityNodeInfoCompat.obtain()
         val clickActionLabel = "click"
         val dismissActionLabel = "dismiss"
-        val accessibilityValue = "checked"
+        val stateDescription = "checked"
         val semanticsModifier = SemanticsModifierCore(1, true, false) {
-            this.accessibilityValue = accessibilityValue
+            this.stateDescription = stateDescription
             onClick(clickActionLabel) { true }
             dismiss(dismissActionLabel) { true }
         }
@@ -141,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 -> {
@@ -154,7 +153,7 @@
                 null
             }
         }
-        assertEquals(accessibilityValue, stateDescription)
+        assertEquals(stateDescription, stateDescriptionResult)
         assertTrue(info.isClickable)
         assertTrue(info.isVisibleToUser)
     }
@@ -164,7 +163,7 @@
         val info = AccessibilityNodeInfoCompat.obtain()
         val setProgressActionLabel = "setProgress"
         val semanticsModifier = SemanticsModifierCore(1, true, false) {
-            accessibilityValueRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
+            stateDescriptionRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
             setProgress(setProgressActionLabel) { true }
         }
         val semanticsNode = SemanticsNode(
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..f85758d 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
@@ -30,7 +30,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class CaptureFocusTest {
     @get:Rule
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/FocusManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt
index ccf7260..5a2f75b 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
@@ -34,7 +34,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FocusManagerAmbientTest {
     @get:Rule
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..c9cf7a2 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
@@ -37,7 +37,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FocusModifierAttachDetachTest {
     @get:Rule
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/FocusObserverTest.kt
index dee66f9..ba18ff1 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/FocusObserverTest.kt
@@ -36,7 +36,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FocusObserverTest {
     @get:Rule
@@ -69,8 +68,7 @@
     fun activeParent_requestFocus() {
         // Arrange.
         lateinit var focusState: FocusState
-        val focusRequester = FocusRequester()
-        val childFocusRequester = FocusRequester()
+        val (focusRequester, childFocusRequester) = FocusRequester.createRefs()
         rule.setFocusableContent {
             Box(
                 modifier = 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..a0cc2da 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
@@ -35,7 +35,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FocusRequesterTest {
     @get:Rule
@@ -259,8 +258,7 @@
         // 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(
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..15b2061 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
@@ -35,7 +35,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class FreeFocusTest {
     @get:Rule
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..ad52c75 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
@@ -36,7 +36,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class OwnerFocusTest {
     @get:Rule
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..fcc4439 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
@@ -33,7 +33,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class ReusedFocusRequesterCaptureFocusTest {
     @get:Rule
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..9251e3f 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
@@ -33,7 +33,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class ReusedFocusRequesterFreeFocusTest {
     @get:Rule
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..50349f4 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
@@ -32,7 +32,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class ReusedFocusRequesterTest {
     @get:Rule
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..43f5cc0 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
@@ -36,7 +36,6 @@
 import org.junit.runner.RunWith
 
 @MediumTest
-@OptIn(ExperimentalFocus::class)
 @RunWith(AndroidJUnit4::class)
 class SetRootFocusTest {
     @get:Rule
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..58a9977 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()
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..2b77b0c 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()
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 f9e3ac6..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,15 +16,14 @@
 
 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
@@ -3131,20 +3130,14 @@
     targetRoot: LayoutNode = LayoutNode()
 ): Owner = MockOwner(position, targetRoot)
 
-@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
@@ -3181,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 4433814..7ca9b32 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
@@ -48,7 +47,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalPointerInput::class, ExperimentalCoroutinesApi::class)
+@OptIn(ExperimentalCoroutinesApi::class)
 class SuspendingPointerInputFilterTest {
     @After
     fun after() {
@@ -235,7 +234,6 @@
 
 private fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
 
-@ExperimentalPointerInput
 private val PointerEvent.firstChange get() = changes.first()
 
 private class PointerInputChangeEmitter(id: Int = 0) {
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 b06582c..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
@@ -63,7 +63,6 @@
 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
@@ -619,7 +618,7 @@
         assertNotNull(innerAndroidComposeView)
         assertTrue(innerAndroidComposeView!!.isAttachedToWindow)
         assertNotNull(node)
-        assertTrue(node!!.isAttached())
+        assertTrue(node!!.isAttached)
 
         rule.runOnIdle { composeContent = false }
 
@@ -627,7 +626,7 @@
         rule.runOnIdle {
             assertFalse(innerAndroidComposeView!!.isAttachedToWindow)
             // the node stays attached after the compose view is detached
-            assertTrue(node!!.isAttached())
+            assertTrue(node!!.isAttached)
         }
     }
 
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/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 6dbf1c4..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
@@ -34,9 +33,9 @@
 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
@@ -85,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) }
@@ -105,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 }) { }
             }
         }
 
@@ -128,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 }) { }
                 }
             }
         }
@@ -148,12 +147,12 @@
         val label3 = "baz"
         rule.setContent {
             SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag1)) {
-                SimpleTestLayout(Modifier.semantics { accessibilityLabel = label1 }) { }
+                SimpleTestLayout(Modifier.semantics { contentDescription = label1 }) { }
                 SimpleTestLayout(Modifier.clearAndSetSemantics {}) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = label2 }) { }
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
                 }
-                SimpleTestLayout(Modifier.clearAndSetSemantics { accessibilityLabel = label3 }) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = label2 }) { }
+                SimpleTestLayout(Modifier.clearAndSetSemantics { contentDescription = label3 }) {
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
                 }
                 SimpleTestLayout(
                     Modifier.semantics(mergeDescendants = true) {}.testTag(tag2)
@@ -174,7 +173,7 @@
         rule.setContent {
             SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(TestTag)) {
                 if (showSubtree.value) {
-                    SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+                    SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
                 }
             }
         }
@@ -184,7 +183,7 @@
         rule.runOnIdle { showSubtree.value = false }
 
         rule.onNodeWithTag(TestTag)
-            .assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
+            .assertDoesNotHaveProperty(SemanticsProperties.ContentDescription)
 
         rule.onAllNodesWithText(label).assertCountEquals(0)
     }
@@ -196,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 }
 
@@ -221,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
@@ -243,7 +242,7 @@
         rule.setContent {
             SimpleTestLayout(
                 Modifier.testTag(TestTag).semantics {
-                    accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                    contentDescription = if (isAfter.value) afterLabel else beforeLabel
                 }
             ) {}
         }
@@ -265,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
                     }
                 ) {}
             }
@@ -289,7 +288,7 @@
                 SimpleTestLayout {
                     SimpleTestLayout(
                         Modifier.testTag(TestTag).semantics {
-                            accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+                            contentDescription = if (isAfter.value) afterLabel else beforeLabel
                         }
                     ) {}
                 }
@@ -313,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
                     }
                 ) {}
             }
@@ -332,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
@@ -354,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 { }) { }
@@ -385,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()
@@ -395,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 df3bb5e..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
@@ -124,7 +124,7 @@
     private fun isSecureFlagEnabledForDialog(): Boolean {
         val owner = rule
             .onNode(isDialog())
-            .fetchSemanticsNode("").layoutNode.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/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..5aa92c0 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
@@ -26,7 +26,6 @@
 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
@@ -59,7 +58,6 @@
 }
 
 @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
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 1d704f3..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,111 +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()
-}
-
-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) {
@@ -162,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 4cc5cf7..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
@@ -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
     }
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 3e63744..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
@@ -64,6 +63,7 @@
 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
@@ -80,20 +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,
+    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
 
@@ -145,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
 
@@ -173,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() {
@@ -228,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
@@ -282,7 +285,7 @@
         isFocusableInTouchMode = true
         clipChildren = false
         ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate)
-        AndroidOwner.onAndroidOwnerCreatedCallback?.invoke(this)
+        ViewRootForTest.onViewCreatedCallback?.invoke(this)
         root.attach(this)
     }
 
@@ -324,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)
     }
 
@@ -503,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)
@@ -513,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) {
@@ -553,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
@@ -575,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) {
@@ -645,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
@@ -665,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 1e8e470..0000000
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidOwner.kt
+++ /dev/null
@@ -1,115 +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.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)
-    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/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 8bd5f11..0e2b0e2 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
@@ -27,6 +27,7 @@
 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
@@ -111,7 +112,7 @@
 // instead.
 @MainThread
 @OptIn(ExperimentalComposeApi::class)
-internal actual fun actualSubcomposeInto(
+internal actual fun subcomposeInto(
     container: LayoutNode,
     parent: CompositionReference,
     composable: @Composable () -> Unit
@@ -139,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)
         }
@@ -168,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) }
@@ -177,12 +178,12 @@
 
 @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>())
         )
@@ -212,7 +213,7 @@
 }
 
 private class WrappedComposition(
-    val owner: AndroidOwner,
+    val owner: AndroidComposeView,
     val original: Composition
 ) : Composition, LifecycleEventObserver {
 
@@ -234,9 +235,17 @@
                     original.setContent {
                         @Suppress("UNCHECKED_CAST")
                         val inspectionTable =
-                            owner.view.getTag(R.id.inspection_slot_table_set) as?
+                            owner.getTag(R.id.inspection_slot_table_set) as?
                                 MutableSet<SlotTable>
-                        inspectionTable?.add(currentComposer.slotTable)
+                                ?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
+                                    as? MutableSet<SlotTable>
+                        if (inspectionTable != null) {
+                            inspectionTable.add(currentComposer.slotTable)
+                            currentComposer.collectParameterInformation()
+                        }
+
+                        LaunchedEffect(owner) { owner.keyboardVisibilityEventLoop() }
+
                         Providers(InspectionTables provides inspectionTable) {
                             ProvideAndroidAmbients(owner, content)
                         }
@@ -283,6 +292,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/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 e12e11f..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
@@ -95,7 +96,7 @@
 
     val dialog = remember(view, density) { DialogWrapper(view, density) }
     dialog.onDismissRequest = onDismissRequest
-    remember(properties) { dialog.setProperties(properties) }
+    SideEffect { dialog.setProperties(properties) }
 
     onActive {
         dialog.show()
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/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..a08c4e3 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.
@@ -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..5188eaa 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,7 +16,6 @@
 
 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
@@ -25,7 +24,6 @@
 /**
  * A [modifier][Modifier.Element] that can be used to observe focus state changes.
  */
-@ExperimentalFocus
 interface FocusObserverModifier : Modifier.Element {
     /**
      * A callback that is called whenever focus state changes.
@@ -33,7 +31,6 @@
     val onFocusChange: (FocusState) -> Unit
 }
 
-@OptIn(ExperimentalFocus::class)
 internal class FocusObserverModifierImpl(
     override val onFocusChange: (FocusState) -> Unit,
     inspectorInfo: InspectorInfo.() -> Unit
@@ -42,7 +39,6 @@
 /**
  * Add this modifier to a component to observe focus state changes.
  */
-@ExperimentalFocus
 fun Modifier.focusObserver(onFocusChange: (FocusState) -> Unit): Modifier {
     return this.then(
         FocusObserverModifierImpl(
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/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/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/ExperimentalPointerInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt
deleted file mode 100644
index 0bfd44c..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt
+++ /dev/null
@@ -1,23 +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.gesture
-
-@RequiresOptIn(
-    "This pointer input API is experimental and is likely to change before becoming " +
-        "stable."
-)
-annotation class ExperimentalPointerInput
\ No newline at end of file
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..f957cd8 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.
@@ -85,7 +84,6 @@
 /**
  * The type of Key Event.
  */
-@ExperimentalKeyInput
 enum class KeyEventType {
     /**
      * Unknown key event.
@@ -110,7 +108,6 @@
     message = "Alt is replaced by KeyEvent.isAltPressed",
     level = DeprecationLevel.WARNING
 )
-@ExperimentalKeyInput
 interface Alt {
     /**
      * Indicates whether the Alt key is pressed.
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 dc18697..db49588 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
@@ -29,7 +29,6 @@
  * 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.
  */
-@ExperimentalKeyInput
 fun Modifier.keyInputFilter(onKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
     inspectorInfo = debugInspectorInfo {
         name = "keyInputFilter"
@@ -49,7 +48,6 @@
  * 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.
  */
-@ExperimentalKeyInput
 fun Modifier.previewKeyInputFilter(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
     inspectorInfo = debugInspectorInfo {
         name = "previewKeyInputFilter"
@@ -59,7 +57,6 @@
     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/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 2c19b0f..21d5ce8 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
@@ -43,11 +42,10 @@
  * Receiver scope for awaiting pointer events in a call to [PointerInputScope.handlePointerInput].
  *
  * 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 {
     /**
@@ -80,25 +78,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][handlePointerInput].
  */
 // 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 +90,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,13 +99,6 @@
     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
@@ -149,7 +125,6 @@
  * pointer input events. Extension functions on [PointerInputScope] or [HandlePointerInputScope]
  * may be defined to perform higher-level gesture detection.
  */
-@ExperimentalPointerInput
 fun Modifier.pointerInput(
     block: suspend PointerInputScope.() -> Unit
 ): Modifier = composed(
@@ -184,7 +159,6 @@
  */
 // 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,21 +170,9 @@
     override val pointerInputFilter: PointerInputFilter
         get() = this
 
-    private var _customEventDispatcher: CustomEventDispatcher? = null
-
     private var currentEvent: PointerEvent? = null
 
-    /**
-     * 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")
-
     override fun onInit(customEventDispatcher: CustomEventDispatcher) {
-        _customEventDispatcher = customEventDispatcher
     }
 
     /**
@@ -328,9 +290,6 @@
     }
 
     override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {
-        forEachCurrentPointerHandler(pass) {
-            it.offerCustomEvent(customEvent, pass)
-        }
     }
 
     override suspend fun <R> handlePointerInput(
@@ -373,7 +332,6 @@
         private val completion: Continuation<R>,
     ) : HandlePointerInputScope, Density by this@SuspendingPointerInputFilter, Continuation<R> {
         private var pointerAwaiter: CancellableContinuation<PointerEvent>? = null
-        private var customAwaiter: CancellableContinuation<CustomEvent>? = null
         private var awaitPass: PointerEventPass = PointerEventPass.Main
 
         override val currentEvent: PointerEvent
@@ -394,21 +352,10 @@
             }
         }
 
-        fun offerCustomEvent(event: CustomEvent, pass: PointerEventPass) {
-            if (pass == awaitPass) {
-                customAwaiter?.run {
-                    customAwaiter = null
-                    resume(event)
-                }
-            }
-        }
-
         // Called to run any finally blocks in the handlePointerInput block
         fun cancel(cause: Throwable?) {
             pointerAwaiter?.cancel(cause)
             pointerAwaiter = null
-            customAwaiter?.cancel(cause)
-            customAwaiter = null
         }
 
         // context must be EmptyCoroutineContext for restricted suspension coroutines
@@ -428,12 +375,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/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/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 8acf50f..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
@@ -33,13 +33,11 @@
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.LayoutState
 import androidx.compose.ui.node.MeasureBlocks
-import androidx.compose.ui.node.isAttached
 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
@@ -79,8 +77,6 @@
             set(AmbientLayoutDirection.current, LayoutEmitHelper.setLayoutDirection)
         }
     )
-
-    state.subcomposeIfRemeasureNotScheduled()
 }
 
 /**
@@ -159,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
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 3289fab..111798f 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,7 +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
@@ -48,6 +47,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
     }
@@ -132,11 +135,14 @@
         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()
 
     override fun findNextKeyInputWrapper() = wrapped.findNextKeyInputWrapper()
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 62e25e8..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
@@ -58,7 +58,7 @@
     }
 
     fun add(node: LayoutNode) {
-        check(node.isAttached())
+        check(node.isAttached)
         if (extraAssertions) {
             val usedDepth = mapOfOriginalDepth[node]
             if (usedDepth == null) {
@@ -71,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/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index f38542b..b75c27c 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,9 @@
 
 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
@@ -59,13 +59,16 @@
 
     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 107116f..aefe6eb 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,13 +17,14 @@
 
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
-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.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDelegatingWrapper
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollModifier
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.input.key.KeyInputModifier
 import androidx.compose.ui.input.pointer.PointerInputFilter
@@ -33,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
@@ -73,10 +76,9 @@
 /**
  * An element in the layout hierarchy, built with compose UI.
  */
-@OptIn(ExperimentalFocus::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
@@ -139,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 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
@@ -155,13 +157,19 @@
     /**
      * The view system [Owner]. This `null` until [attach] is called
      */
-    var owner: Owner? = null
+    internal var owner: Owner? = null
         private set
 
     /**
+     * Returns true if this [LayoutNode] currently has an [LayoutNode.owner].  Semantically,
+     * this means that the LayoutNode is currently a part of a component tree.
+     */
+    override val isAttached: Boolean get() = owner != null
+
+    /**
      * The tree depth of the [LayoutNode]. This is valid only when it is attached to a hierarchy.
      */
-    var depth: Int = 0
+    internal var depth: Int = 0
 
     /**
      * The layout state the node is currently in.
@@ -290,7 +298,7 @@
      * 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"
         }
@@ -318,7 +326,6 @@
         parent?.requestRemeasure()
         innerLayoutNodeWrapper.attach()
         forEachDelegate { it.attach() }
-        updateInnerLayerWrapper()
         onAttach?.invoke(owner)
     }
 
@@ -327,7 +334,7 @@
      * 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!"
@@ -348,7 +355,6 @@
         }
         owner.onDetach(this)
         this.owner = null
-        _innerLayerWrapper = null
         depth = 0
         _foldedChildren.forEach { child ->
             child.detach()
@@ -380,7 +386,7 @@
         }
 
     override val isValid: Boolean
-        get() = isAttached()
+        get() = isAttached
 
     override fun toString(): String {
         return "${simpleIdentityToString(this, null)} children: ${children.size} " +
@@ -440,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
@@ -451,7 +457,7 @@
     /**
      * 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]
@@ -466,7 +472,7 @@
     /**
      * The layout direction of the layout node.
      */
-    var layoutDirection: LayoutDirection = LayoutDirection.Ltr
+    internal var layoutDirection: LayoutDirection = LayoutDirection.Ltr
         set(value) {
             if (field != value) {
                 field = value
@@ -477,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
@@ -499,7 +505,7 @@
     /**
      * Whether or not this [LayoutNode] and all of its parents have been placed in the hierarchy.
      */
-    var isPlaced: Boolean = false
+    override var isPlaced: Boolean = false
         private set
 
     /**
@@ -557,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)
@@ -576,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)
@@ -602,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) {
@@ -613,10 +632,11 @@
             val invalidateParentLayer = shouldInvalidateParentLayer()
 
             copyWrappersToCache()
+            markReusedModifiers(value)
 
             // Rebuild LayoutNodeWrapper
             val oldOuterWrapper = outerMeasurablePlaceable.outerWrapper
-            if (outerSemantics != null && isAttached()) {
+            if (outerSemantics != null && isAttached) {
                 owner!!.onSemanticsChange()
             }
             val addedCallback = hasNewPositioningCallback()
@@ -663,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)
                     }
@@ -679,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
@@ -727,7 +747,7 @@
     /**
      * Coordinates of just the contents of the [LayoutNode], after being affected by all modifiers.
      */
-    val coordinates: LayoutCoordinates
+    override val coordinates: LayoutCoordinates
         get() = innerLayoutNodeWrapper
 
     /**
@@ -757,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
@@ -826,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
@@ -1113,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<*>
@@ -1146,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) {
@@ -1182,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)
@@ -1237,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
     }
 
     /**
@@ -1262,6 +1280,9 @@
         }
     }
 
+    override val parentInfo: LayoutInfo?
+        get() = parent
+
     internal companion object {
         private val ErrorMeasureBlocks: NoIntrinsicsMeasureBlocks =
             object : NoIntrinsicsMeasureBlocks(
@@ -1329,22 +1350,6 @@
 }
 
 /**
- * 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")
-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.
  */
 internal fun LayoutNode.requireOwner(): Owner {
@@ -1356,8 +1361,9 @@
 }
 
 /**
- * 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].
  */
 internal fun LayoutNode.add(child: LayoutNode) {
     insertAt(children.size, child)
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 6ad3889..a32ad2c 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
@@ -65,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
         }
@@ -109,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
 
@@ -158,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
@@ -222,6 +222,7 @@
                     move(position)
                 }
                 updateLayerParameters()
+                layoutNode.innerLayerWrapperIsDirty = true
                 invalidateParentLayer()
             } else if (blockHasBeenChanged) {
                 updateLayerParameters()
@@ -229,7 +230,7 @@
         } else {
             layer?.let {
                 it.destroy()
-
+                layoutNode.innerLayerWrapperIsDirty = true
                 invalidateParentLayer()
             }
             layer = null
@@ -499,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?
 
@@ -521,7 +548,6 @@
      * 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)
 
     /**
@@ -573,12 +599,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/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 126c75e..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
@@ -189,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
@@ -225,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/ModifiedFocusNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
index 7da03ef..cccc303 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,9 +27,6 @@
 import androidx.compose.ui.focus.searchChildrenForFocusNode
 import androidx.compose.ui.util.fastForEach
 
-@OptIn(
-    ExperimentalFocus::class,
-)
 internal class ModifiedFocusNode(
     wrapped: LayoutNodeWrapper,
     modifier: FocusModifier
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/ModifiedFocusObserverNode.kt
index 2aa55e8..92cf057 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/ModifiedFocusObserverNode.kt
@@ -17,12 +17,10 @@
 package androidx.compose.ui.node
 
 import androidx.compose.ui.FocusObserverModifier
-import androidx.compose.ui.focus.ExperimentalFocus
 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(
     wrapped: LayoutNodeWrapper,
     modifier: FocusObserverModifier
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 03f8140..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
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 d01fe4c..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
@@ -66,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/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index b049c55..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
@@ -67,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
@@ -83,7 +84,6 @@
     /**
      * Provide a focus manager that controls focus within Compose.
      */
-    @ExperimentalFocus
     val focusManager: FocusManager
 
     /**
@@ -114,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.
@@ -150,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/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 c741795..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,20 +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.LayoutNode
 import androidx.compose.ui.util.annotation.MainThread
 
-internal expect fun actualSubcomposeInto(
-    container: LayoutNode,
-    parent: CompositionReference,
-    composable: @Composable () -> Unit
-): Composition
-
-@OptIn(ExperimentalComposeApi::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/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 8b28026..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,8 +23,10 @@
 import androidx.compose.ui.layout.globalBounds
 import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.layout.LayoutInfo
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNodeWrapper
+import androidx.compose.ui.node.Owner
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
 
@@ -66,9 +68,20 @@
     val id: Int = layoutNodeWrapper.modifier.id
 
     /**
+     * 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.
      */
-    val layoutNode: LayoutNode = layoutNodeWrapper.layoutNode
+    internal val layoutNode: LayoutNode = layoutNodeWrapper.layoutNode
 
     // GEOMETRY
 
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 e6d3278..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
@@ -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")
@@ -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/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
index f4131a1..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
@@ -335,7 +334,6 @@
      *
      * @param content Composable content of the window.
      */
-    @OptIn(ExperimentalKeyInput::class)
     override fun show(content: @Composable () -> Unit) {
         if (invoker != null) {
             invoker!!.lockWindow()
@@ -387,6 +385,5 @@
     /**
      * Gets the Keyboard object of the window.
      */
-    @ExperimentalKeyInput
     val keyboard: Keyboard = Keyboard()
 }
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..ed20405 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
@@ -54,7 +53,6 @@
 }
 
 @Suppress("DEPRECATION")
-@OptIn(ExperimentalKeyInput::class)
 internal inline class AltDesktop(val keyEvent: KeyEventAwt) : Alt {
 
     override val isLeftAltPressed
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..9be1321 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()
 ) {
@@ -133,7 +132,6 @@
  * @see [keyInputFilter]
  * @see [androidx.compose.ui.platform.Keyboard] to define window-scoped shortcuts
  */
-@ExperimentalKeyInput
 @Composable
 fun Modifier.shortcuts(builder: (ShortcutsBuilderScope).() -> Unit) = composed {
     val instance = remember { ShortcutsInstance() }
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 3510b9c..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
@@ -57,8 +56,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 
 @OptIn(
-    ExperimentalFocus::class,
-    ExperimentalKeyInput::class,
+    ExperimentalComposeUiApi::class,
     ExperimentalComposeApi::class,
     InternalCoreApi::class
 )
@@ -177,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 d615e5e..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
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 0e0e99e..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
@@ -24,7 +24,11 @@
 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 9c7e461..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,10 +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.ExperimentalComposeUiApi
 import androidx.compose.ui.node.LayoutNode
 
-@OptIn(ExperimentalComposeApi::class, ExperimentalKeyInput::class)
+@OptIn(ExperimentalComposeApi::class)
 fun DesktopOwner.setContent(content: @Composable () -> Unit): Composition {
     GlobalSnapshotManager.ensureStarted()
 
@@ -46,8 +46,7 @@
 
     return composition
 }
-
-@OptIn(ExperimentalKeyInput::class)
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 private fun ProvideDesktopAmbients(owner: DesktopOwner, content: @Composable () -> Unit) {
     Providers(
@@ -64,7 +63,7 @@
 }
 
 @OptIn(ExperimentalComposeApi::class)
-internal actual fun actualSubcomposeInto(
+internal actual fun subcomposeInto(
     container: LayoutNode,
     parent: CompositionReference,
     composable: @Composable () -> Unit
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/DesktopOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
index 87ad037..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
@@ -53,7 +53,6 @@
 import org.junit.Test
 
 @OptIn(
-    ExperimentalLayoutNodeApi::class,
     ExperimentalComposeApi::class,
     ExperimentalCoroutinesApi::class
 )
@@ -289,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/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
index 0a4c2b3..7485666 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
@@ -241,7 +241,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/FocusObserverModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
index 682eed0..4684ec3 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui
 
-import androidx.compose.ui.focus.ExperimentalFocus
 import androidx.compose.ui.focus.FocusState
 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 onFocusChange: (FocusState) -> Unit = {}
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..42294c6 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/focus/FocusManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
index 4caf023..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
@@ -32,9 +32,6 @@
 import org.junit.runners.Parameterized
 import kotlin.jvm.JvmStatic
 
-@OptIn(
-    ExperimentalFocus::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..75e35df 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,7 +36,6 @@
         isDebugInspectorInfoEnabled = false
     }
 
-    @OptIn(ExperimentalKeyInput::class)
     @Test
     fun testInspectorValueForKeyInputFilter() {
         val onKeyEvent: (KeyEvent) -> Boolean = { true }
@@ -48,7 +47,6 @@
         )
     }
 
-    @OptIn(ExperimentalKeyInput::class)
     @Test
     fun testInspectorValueForPreviewKeyInputFilter() {
         val onPreviewKeyEvent: (KeyEvent) -> Boolean = { true }
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 625a6d3..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,6 +15,7 @@
  */
 package androidx.compose.ui.node
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
@@ -22,7 +23,6 @@
 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
@@ -79,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 })
     }
 
@@ -1633,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()
@@ -1654,10 +1692,7 @@
         PointerInputModifier
 }
 
-@OptIn(
-    ExperimentalFocus::class,
-    InternalCoreApi::class
-)
+@OptIn(InternalCoreApi::class)
 private class MockOwner(
     val position: IntOffset = IntOffset.Zero,
     override val root: LayoutNode = LayoutNode()
@@ -1672,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
@@ -1702,8 +1739,6 @@
         layoutNode.layoutState = LayoutNode.LayoutState.NeedsRelayout
     }
 
-    override val hasPendingMeasureOrLayout = false
-
     override fun onAttach(node: LayoutNode) {
         onAttachParams += node
     }
@@ -1716,7 +1751,6 @@
 
     override fun requestFocus(): Boolean = false
 
-    @ExperimentalKeyInput
     override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
 
     override fun measureAndLayout() {
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/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 729b498..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();
   }
 
@@ -2296,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);
   }
@@ -2602,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();
@@ -2714,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);
@@ -2767,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!);
@@ -2805,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);
@@ -3776,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();
@@ -3845,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/view/ContentInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
new file mode 100644
index 0000000..15b5fa7
--- /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/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 c858653..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 8bc10fe..83003c2 100644
--- a/datastore/datastore-rxjava2/build.gradle
+++ b/datastore/datastore-rxjava2/build.gradle
@@ -27,18 +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)
@@ -53,6 +47,11 @@
     testImplementation(KOTLIN_COROUTINES_TEST)
     testImplementation(TRUTH)
     testImplementation(project(":internal-testutils-truth"))
+
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(project(":internal-testutils-truth"))
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
 }
 
 androidx {
@@ -63,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
new file mode 100644
index 0000000..cc1f3493
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
@@ -0,0 +1,37 @@
+/*
+ * 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
+
+@Suppress("UNCHECKED_CAST")
+internal fun <T : Throwable?> assertThrows(
+    expectedType: Class<T>,
+    runnable: Runnable
+): T {
+    try {
+        runnable.run()
+    } catch (t: Throwable) {
+        if (!expectedType.isInstance(t)) {
+            throw RuntimeException(t)
+        }
+        return t as T
+    }
+    throw AssertionError(
+        String.format(
+            "Expected %s wasn't thrown",
+            expectedType.simpleName
+        )
+    )
+}
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
new file mode 100644
index 0000000..2d3a0ae
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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 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;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+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;
+
+public class RxDataStoreBuilderTest {
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private static Single<Byte> incrementByte(Byte byteIn) {
+        return Single.just(++byteIn);
+    }
+
+    @Test
+    public void testConstructWithProduceFile() throws Exception {
+        File file = tempFolder.newFile();
+        DataStore<Byte> dataStore =
+                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>(() -> 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>(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>(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>(() -> 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);
+    }
+
+    @Test
+    public void testSpecifiedSchedulerIsUser() throws Exception {
+        Scheduler singleThreadedScheduler =
+                Schedulers.from(Executors.newSingleThreadExecutor(new ThreadFactory() {
+                    @Override
+                    public Thread newThread(Runnable r) {
+                        return new Thread(r, "TestingThread");
+                    }
+                }));
+
+
+        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);
+
+
+        DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+                () -> tempFolder.newFile(),
+                testingSerializer)
+                .setCorruptionHandler(replaceFileCorruptionHandler)
+                .build();
+        assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(99);
+    }
+}
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 4306a57..c35170b 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -400,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\)
@@ -408,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
@@ -446,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
@@ -467,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\!
@@ -610,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:
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-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 77b8b66..7e1ec2e 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -684,6 +684,22 @@
         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
+        }
     }
 
     /**
@@ -757,29 +773,80 @@
         assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
     }
 
-    // TODO: Add tests for other variations (e.g. single/double digit number strings)
     @Test
     @LargeTest
-    public void testParsingSubsec() throws IOException {
+    public void testSubsec() throws IOException {
         File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
         ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
-        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 0ms */ "000000");
+
+        // 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();
 
-        // Check that TAG_SUBSEC_TIME values starting with zero are supported.
-        // Note: getDateTime() supports only up to 1/1000th of a second.
-        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 1ms */ "001000");
-        exif.saveAttributes();
-        assertEquals(currentDateTimeValue + 1, (long) exif.getDateTime());
-
-        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 10ms */ "010000");
-        exif.saveAttributes();
-        assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
-
-        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 100ms */ "100000");
+        // 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/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 76d6f81..4099177 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -5151,9 +5151,21 @@
      */
     @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);
     }
 
     /**
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/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/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 2cc8680..f3e9d37 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -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/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/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 9e7ec3d..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
@@ -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/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/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/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 d72a0d9a..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);
diff --git a/paging/common/api/public_plus_experimental_current.txt b/paging/common/api/public_plus_experimental_current.txt
index f286ae0..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);
diff --git a/paging/common/api/restricted_current.txt b/paging/common/api/restricted_current.txt
index d72a0d9a..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);
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/LegacyPagingSource.kt b/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt
index 43283af..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,25 @@
  * 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>() {
     private var pageSize: Int = PAGE_SIZE_NOT_SET
-    // 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()
-            }
+
+    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()
         }
     }
 
@@ -118,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) {
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 35a9b12..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,7 +208,7 @@
         refreshEvents.send(false)
     }
 
-    private fun generateNewPagingSource(
+    private suspend fun generateNewPagingSource(
         previousPagingSource: PagingSource<Key, Value>?
     ): PagingSource<Key, Value> {
         val pagingSource = pagingSourceFactory()
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
index 0d2dee2..020ae17 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
@@ -303,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 f54c929..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
@@ -181,9 +180,7 @@
                         config.enablePlaceholders,
                     )
                     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
@@ -479,8 +476,11 @@
         @Suppress("DEPRECATION")
         fun build(): PagedList<Value> {
             val fetchDispatcher = fetchDispatcher ?: Dispatchers.IO
-            val pagingSource = pagingSource ?: dataSource?.let {
-                LegacyPagingSource { it }.also {
+            val pagingSource = pagingSource ?: dataSource?.let { dataSource ->
+                LegacyPagingSource(
+                    fetchDispatcher = fetchDispatcher,
+                    dataSource = dataSource
+                ).also {
                     it.setPageSize(config.pageSize)
                 }
             }
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 3f64938..4db4e26 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
@@ -315,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>()
@@ -335,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 a6941fc..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 }
@@ -313,10 +312,6 @@
         }
     }
 
-    private fun verifyDropCallback(callback: Callback, position: Int) {
-        verifyDropCallback(callback, position, position)
-    }
-
     @Test
     fun append() {
         val pagedList = createCountedPagedList(0)
@@ -463,7 +458,7 @@
         drain()
         verifyRange(20, 60, pagedList)
         verifyCallback(callback, 60)
-        verifyDropCallback(callback, 0)
+        verifyDropCallback(callback, 0, 0)
         verifyNoMoreInteractions(callback)
     }
 
@@ -1050,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 b93aea5..6e8ede2 100644
--- a/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
@@ -139,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 5b8ea29..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))
-        }
-
-        // 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 bd766f3..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
@@ -126,8 +137,8 @@
 
         @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 a5b05f9..6f90f6b 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -256,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 151a55d..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/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 7ed4007..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.
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/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 1a8211d..2959747 100644
--- a/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
+++ b/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
@@ -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/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/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/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/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/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/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/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/KspTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 60339a5..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
@@ -317,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 734f3b1..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/ResolverExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
index e8a80d4..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
@@ -25,7 +25,6 @@
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
 import com.google.devtools.ksp.symbol.KSPropertyAccessor
 import com.google.devtools.ksp.symbol.KSPropertyDeclaration
-import com.google.devtools.ksp.symbol.Origin
 
 internal fun Resolver.findClass(qName: String) = getClassDeclarationByName(
     getKSNameFromString(qName)
@@ -92,12 +91,7 @@
 }
 
 private fun KSPropertyDeclaration.overrides(other: KSPropertyDeclaration): Boolean {
-    val overridee = try {
-        findOverridee()
-    } catch (ex: NoSuchElementException) {
-        // workaround for https://github.com/google/ksp/issues/174
-        null
-    }
+    val overridee = findOverridee()
     if (overridee == other) {
         return true
     }
@@ -108,10 +102,6 @@
 internal fun Resolver.safeGetJvmName(
     declaration: KSFunctionDeclaration
 ): String {
-    if (declaration.origin == Origin.JAVA) {
-        // https://github.com/google/ksp/issues/170
-        return declaration.simpleName.asString()
-    }
     return try {
         getJvmName(declaration)
     } catch (ignored: ClassCastException) {
@@ -126,10 +116,6 @@
     accessor: KSPropertyAccessor,
     fallback: () -> String
 ): String {
-    if (accessor.origin == Origin.JAVA) {
-        // https://github.com/google/ksp/issues/170
-        return fallback()
-    }
     return try {
         getJvmName(accessor)
     } catch (ignored: ClassCastException) {
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 49ad742..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
@@ -22,7 +22,7 @@
 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.runProcessorTestIncludingKsp
+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
@@ -291,7 +291,7 @@
     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, ignoreInheritedMethods)
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(source)
         ) { invocation ->
             val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
@@ -316,8 +316,7 @@
     ): List<String> {
         lateinit var result: List<String>
         runKaptTest(
-            sources = listOf(source),
-            succeed = true
+            sources = listOf(source)
         ) { invocation ->
             val (target, methods) = invocation.getOverrideTestTargets(
                 ignoreInheritedMethods
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 d84af6e..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,8 +22,8 @@
 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 androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.TypeName
@@ -36,7 +36,7 @@
 class XElementTest {
     @Test
     fun modifiers() {
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(
                 Source.java(
                     "foo.bar.Baz",
@@ -146,7 +146,7 @@
                 }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(genericBase, boundedChild)
         ) {
             fun validateElement(element: XTypeElement, tTypeName: TypeName, rTypeName: TypeName) {
@@ -210,7 +210,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -257,7 +257,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("java.lang.Object")
@@ -280,7 +280,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(subject)
         ) {
             val inner = ClassName.get("foo.bar", "Baz.Inner")
@@ -317,7 +317,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -350,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")
@@ -371,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 a270b74..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")
@@ -108,7 +108,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(subject)
         ) {
             val element = it.processingEnv.requireTypeElement("Subject")
@@ -138,7 +138,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(subject)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -182,7 +182,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(src)
         ) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("Subject")
@@ -269,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
@@ -323,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()
@@ -386,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 859e553..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,8 @@
 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 androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.TypeName
 import org.junit.Test
@@ -69,7 +69,7 @@
             """.trimIndent()
         )
         // TODO run with KSP once https://github.com/google/ksp/issues/167 is fixed
-        runProcessorTest(
+        runProcessorTestWithoutKsp(
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -164,7 +164,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(
+        runProcessorTest(
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -257,7 +257,7 @@
 
     @Test
     fun changeNullability_primitives() {
-        runProcessorTestIncludingKsp { invocation ->
+        runProcessorTest { invocation ->
             PRIMITIVE_TYPES.forEach { primitiveTypeName ->
                 val primitive = invocation.processingEnv.requireType(primitiveTypeName)
                 assertThat(primitive.nullability).isEqualTo(NONNULL)
@@ -295,7 +295,7 @@
                 }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(javaSrc, kotlinSrc)) { invocation ->
+        runProcessorTest(sources = listOf(javaSrc, kotlinSrc)) { invocation ->
             listOf("KotlinClass", "JavaClass").forEach {
                 val subject = invocation.processingEnv.requireTypeElement(it)
                     .getField("subject").type
@@ -316,7 +316,7 @@
 
     @Test
     fun changeNullability_declared() {
-        runProcessorTestIncludingKsp { invocation ->
+        runProcessorTest { invocation ->
             val subject = invocation.processingEnv.requireType("java.util.List")
             subject.makeNullable().let {
                 assertThat(it.nullability).isEqualTo(NULLABLE)
@@ -337,7 +337,7 @@
 
     @Test
     fun changeNullability_arrayTypes() {
-        runProcessorTestIncludingKsp { invocation ->
+        runProcessorTest { invocation ->
             val subject = invocation.processingEnv.getArrayType(
                 invocation.processingEnv.requireType("java.util.List")
             )
@@ -368,7 +368,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+        runProcessorTest(sources = listOf(src)) { invocation ->
             val voidType = invocation.processingEnv.requireTypeElement("Foo")
                 .getMethod("subject").returnType
             assertThat(voidType.typeName).isEqualTo(TypeName.VOID)
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 1bc7787..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
@@ -48,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 ->
@@ -121,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()
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 437bdc3..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,7 +437,7 @@
             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"
@@ -471,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"
@@ -520,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",
@@ -576,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()
@@ -623,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/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/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/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/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/ui/ui-test-font/src/main/res/font/invalid_font.ttf b/security/security-app-authenticator/api/res-current.txt
similarity index 100%
copy from compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf
copy 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 a04f8c7..661f378 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -432,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/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/studiow b/studiow
index 577071b..2dc12360 100755
--- a/studiow
+++ b/studiow
@@ -1,16 +1,48 @@
 #!/usr/bin/env bash
-if [ -n "$1" ]; then
-  export ANDROIDX_PROJECTS=${1^^}
-else
-  export ANDROIDX_PROJECTS=MAIN
-  echo "Supported projects sets include:"
-  echo "- MAIN for non-Compose Jetpack libraries"
-  echo "- COMPOSE for Compose and dependencies"
-  echo "- FLAN for Fragment, Lifecycle, Activity, and Navigation"
-  echo "- ALL for all libraries"
-  echo
-  echo "No project set specified, using MAIN..."
-fi
-shift
-source gradlew studio "$@"
 
+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-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/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-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index ee7fa64..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;
@@ -84,7 +86,9 @@
 
   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();
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 5b9d604..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;
@@ -84,7 +86,9 @@
 
   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();
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index be317d9..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;
@@ -84,7 +86,9 @@
 
   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();
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 7a6a332..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,8 +33,10 @@
 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
@@ -50,9 +53,11 @@
 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
@@ -151,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,
@@ -164,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,
@@ -264,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,
@@ -304,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,
@@ -338,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[
@@ -359,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
@@ -379,6 +449,9 @@
             ComplicationType.SMALL_IMAGE
         )
         assertTrue(rightComplicationDetails.isEnabled)
+        assertThat(rightComplicationDetails.currentType).isEqualTo(
+            ComplicationType.LONG_TEXT
+        )
 
         interactiveInstance.close()
     }
@@ -530,7 +603,12 @@
         )
 
         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,
@@ -544,6 +622,36 @@
             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 847643a..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
@@ -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/InteractiveWatchFaceWcsClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
index 1205802..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,11 +20,13 @@
 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
@@ -107,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. */
@@ -177,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-data/api/restricted_current.txt b/wear/wear-watchface-data/api/restricted_current.txt
index f95a226..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);
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/api/current.txt b/wear/wear-watchface/api/current.txt
index d3c5678..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 drawOutline(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();
@@ -67,12 +67,12 @@
 
   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 {
@@ -138,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;
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index d3c5678..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 drawOutline(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();
@@ -67,12 +67,12 @@
 
   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 {
@@ -138,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;
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 6eddf21..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 drawOutline(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();
@@ -67,12 +67,12 @@
 
   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 {
@@ -163,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;
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 2c9375c..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
@@ -248,7 +249,8 @@
                         RenderParameters(
                             DrawMode.AMBIENT,
                             RenderParameters.DRAW_ALL_LAYERS,
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -281,7 +283,8 @@
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             RenderParameters.DRAW_ALL_LAYERS,
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -333,7 +336,8 @@
                                 Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
                                 Layer.TOP_LAYER to LayerMode.DRAW
                             ),
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -370,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,
@@ -418,7 +423,8 @@
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             RenderParameters.DRAW_ALL_LAYERS,
-                            null
+                            null,
+                            Color.RED
                         ).toWireFormat(),
                         100,
                         123456789,
@@ -450,9 +456,9 @@
             // 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/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 8454576..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,6 +23,7 @@
 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
@@ -163,7 +164,7 @@
                 if (renderParameters.highlightedComplicationId == null ||
                     renderParameters.highlightedComplicationId == idAndData?.complicationId
                 ) {
-                    drawOutline(canvas, bounds, calendar)
+                    drawOutline(canvas, bounds, calendar, renderParameters.highlightTint)
                 }
             }
             LayerMode.HIDE -> return
@@ -174,9 +175,14 @@
     public open fun drawOutline(
         canvas: Canvas,
         bounds: Rect,
-        calendar: Calendar
+        calendar: Calendar,
+        @ColorInt color: Int
     ) {
-        ComplicationOutlineRenderer.drawComplicationSelectOutline(canvas, bounds)
+        ComplicationOutlineRenderer.drawComplicationSelectOutline(
+            canvas,
+            bounds,
+            color
+        )
     }
 
     /**
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 4d5bb5d..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
 
@@ -38,12 +38,16 @@
             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/ObservableWatchData.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
index 52ab4b0..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
@@ -21,7 +21,7 @@
 /**
  * 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?) {
 
@@ -100,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 bb4754b..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
@@ -17,6 +17,7 @@
 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
@@ -135,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
@@ -249,8 +250,9 @@
     }
 
     /**
-     * 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() {
         if (this::watchFaceHostApi.isInitialized) {
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 a101a9c..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
@@ -331,35 +329,6 @@
     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,
@@ -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
@@ -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 5a99ffc..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
@@ -237,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].
@@ -263,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.
@@ -303,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
@@ -345,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()
@@ -399,7 +380,6 @@
                 systemState.inAmbientMode != mutableWatchState.isAmbient.value
             ) {
                 mutableWatchState.isAmbient.value = systemState.inAmbientMode
-                updateTimeTickReceiver()
             }
 
             if (firstSetSystemState ||
@@ -458,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()
                         )
                     )
                 }
@@ -660,11 +643,6 @@
                 choreographer.removeFrameCallback(frameCallback)
             }
 
-            if (timeTickRegistered) {
-                timeTickRegistered = false
-                unregisterReceiver(timeTickReceiver)
-            }
-
             if (this::watchFaceImpl.isInitialized) {
                 watchFaceImpl.onDestroy()
             }
@@ -924,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)
 
@@ -979,7 +922,6 @@
             }
 
             mutableWatchState.isVisible.value = visible
-            updateTimeTickReceiver()
             pendingVisibilityChanged = null
         }
 
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 1165fbe9..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
+    }
 }
 
 /**
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 c8cd451..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
@@ -329,6 +329,7 @@
         sendImmutableProperties(engineWrapper, hasLowBitAmbient, hasBurnInProtection)
 
         watchFaceImpl = engineWrapper.watchFaceImpl
+        testWatchFaceService.setIsVisible(true)
     }
 
     private fun initWallpaperInteractiveWatchFaceInstance(
@@ -378,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) {
@@ -518,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)
@@ -546,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)
@@ -875,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
         )
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 c6f4bf9..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();
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index ebd25b4..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();
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 5640c27..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();
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/WearableRecyclerViewTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
index 3158fbd..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,6 @@
 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;
 
@@ -39,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;
 
@@ -65,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() {
@@ -75,100 +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);
-            }
-        });
 
-        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);
-                Activity activity = mActivityRule.getActivity();
-                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(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.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                WearableRecyclerView wrv =
-                        (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
-                wrv.setEdgeItemsCenteringEnabled(false);
-            }
+        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(0, 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());
 
-            }
+                }
+            });
         });
     }
 
     @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);
-                Activity activity = mActivityRule.getActivity();
-                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);
+                    // 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());
+                    }
                 }
-            }
+            });
         });
     }
 
@@ -176,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
@@ -200,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/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 5e43a73..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/aidl/androidx/car/app/IOnSelectedListener.aidl b/window/window/src/test/java/androidx/window/ActivityTestUtil.java
similarity index 65%
copy from car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
copy to window/window/src/test/java/androidx/window/ActivityTestUtil.java
index 2896a83..2d6d5d7 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/window/window/src/test/java/androidx/window/ActivityTestUtil.java
@@ -14,11 +14,18 @@
  * limitations under the License.
  */
 
-package androidx.car.app;
+package androidx.window;
 
-import androidx.car.app.IOnDoneCallback;
+import android.app.Activity;
+import android.os.IBinder;
 
-/** @hide */
-oneway interface IOnSelectedListener {
-  void onSelected(int index, IOnDoneCallback callback) = 1;
+import androidx.annotation.NonNull;
+
+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();
+    }
+}