[go: nahoru, domu]

Merge "Ensure destinations are linked properly on nav back stack" into androidx-master-dev
diff --git a/animation/build.gradle b/animation/build.gradle
index 743350e..f0a633e 100644
--- a/animation/build.gradle
+++ b/animation/build.gradle
@@ -26,7 +26,7 @@
 dependencies {
     implementation("androidx.annotation:annotation:1.1.0-rc01")
     implementation(project(":core"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT, libs.exclude_for_espresso)
     androidTestImplementation(ANDROIDX_TEST_RULES, libs.exclude_for_espresso)
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/testutils/BaseTestActivity.java b/appcompat/src/androidTest/java/androidx/appcompat/testutils/BaseTestActivity.java
index 1b20e5c..1827727 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/testutils/BaseTestActivity.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/testutils/BaseTestActivity.java
@@ -27,6 +27,7 @@
 import androidx.appcompat.app.AppCompatCallback;
 import androidx.appcompat.test.R;
 import androidx.appcompat.view.ActionMode;
+import androidx.testutils.LocaleTestUtils;
 import androidx.testutils.RecreatedAppCompatActivity;
 
 public abstract class BaseTestActivity extends RecreatedAppCompatActivity {
@@ -48,10 +49,19 @@
     private boolean mDestroyed;
 
     private AppCompatCallback mAppCompatCallback;
+    private static final String EXTRA_LANGUAGE = "language";
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        final LocaleTestUtils locale = new LocaleTestUtils(this);
+        if (getIntent().hasExtra(EXTRA_LANGUAGE)) {
+            locale.setLocale(LocaleTestUtils.RTL_LANGUAGE);
+        } else {
+            locale.setLocale(LocaleTestUtils.DEFAULT_TEST_LANGUAGE);
+        }
+
         overridePendingTransition(0, 0);
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
         final int contentView = getContentViewLayoutResId();
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
index 7c45fa9..dea4a0f6 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
@@ -32,8 +32,11 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
+import android.os.Build;
 import android.os.SystemClock;
 import android.view.View;
 
@@ -51,10 +54,14 @@
 import androidx.test.espresso.action.Swipe;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.LocaleTestUtils;
 import androidx.testutils.PollingCheck;
 
+import org.hamcrest.Description;
 import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
 import org.junit.After;
 import org.junit.Test;
 
@@ -65,7 +72,7 @@
 @LargeTest
 public class AppCompatSpinnerTest
         extends AppCompatBaseViewTest<AppCompatSpinnerActivity, AppCompatSpinner> {
-    private static final String EARTH = "Earth";
+    private static final String >
     private Instrumentation mInstrumentation;
 
     public AppCompatSpinnerTest() {
@@ -138,13 +145,11 @@
         waitUntilPopupIsHidden(spinner);
     }
 
-    @LargeTest
     @Test
     public void testPopupThemingFromXmlAttribute() {
         verifySpinnerPopupTheming(R.id.view_magenta_themed_popup, R.color.test_magenta, true);
     }
 
-    @LargeTest
     @Test
     public void testUnthemedPopupRuntimeTheming() {
         final AppCompatSpinner spinner =
@@ -158,7 +163,6 @@
         verifySpinnerPopupTheming(R.id.view_unthemed_popup, R.color.test_green, false);
     }
 
-    @LargeTest
     @Test
     public void testThemedPopupRuntimeTheming() {
         final AppCompatSpinner spinner =
@@ -191,13 +195,11 @@
         assertThat(dialogPopup.mPopup, instanceOf(AlertDialog.class));
     }
 
-    @LargeTest
     @Test
     public void testChangeOrientationDialogPopupPersists() {
         verifyChangeOrientationPopupPersists(R.id.spinner_dialog_popup, true);
     }
 
-    @LargeTest
     @Test
     public void testChangeOrientationDropdownPopupPersists() {
         verifyChangeOrientationPopupPersists(R.id.spinner_dropdown_popup, false);
@@ -213,10 +215,11 @@
         Instrumentation.ActivityMonitor monitor =
                 new Instrumentation.ActivityMonitor(mActivity.getClass().getName(), null, false);
         mInstrumentation.addMonitor(monitor);
+
         mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
         SystemClock.sleep(250);
-        mInstrumentation.waitForIdleSync();
 
+        mInstrumentation.waitForIdleSync();
         mActivity = (AppCompatSpinnerActivity) mInstrumentation.waitForMonitor(monitor);
 
         // Now we can get the new (post-rotation) instance of our spinner
@@ -225,7 +228,6 @@
         assertTrue(newSpinner.getInternalPopup().isShowing());
     }
 
-    @LargeTest
     @Test
     public void testSlowScroll() {
         final AppCompatSpinner spinner = mContainer
@@ -247,6 +249,69 @@
         onView(withText(secondItem)).check(doesNotExist());
     }
 
+    @Test
+    public void testHorizontalOffset() {
+        checkOffsetIsCorrect(500, false, false);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    public void testHorizontalOffsetRtl() {
+        setRtl();
+        checkOffsetIsCorrect(200, false, true);
+    }
+
+    @Test
+    public void testVerticalOffset() {
+        checkOffsetIsCorrect(100, true, false);
+    }
+
+    private void checkOffsetIsCorrect(
+            final int offset,
+            final boolean isVerticalOffset,
+            final boolean isRtl) {
+        int spinnerId = R.id.spinner_dropdown_popup_small;
+
+        final AppCompatSpinner spinner = mContainer.findViewById(spinnerId);
+        if (isVerticalOffset) {
+            spinner.setDropDownVerticalOffset(offset);
+        } else {
+            spinner.setDropDownHorizontalOffset(offset);
+        }
+
+        onView(withId(spinnerId)).perform(click());
+        SystemClock.sleep(250);
+
+        int computedOffset;
+        if (isVerticalOffset) {
+            int[] location = new int[2];
+            spinner.getLocationOnScreen(location);
+
+            computedOffset = location[1] + offset;
+        } else {
+            if (isRtl) {
+                int[] location = new int[2];
+                spinner.getLocationOnScreen(location);
+                final AppCompatSpinner.SpinnerPopup spinnerPopup = spinner.getInternalPopup();
+                AppCompatSpinner.DropdownPopup dropdownPopup =
+                        (AppCompatSpinner.DropdownPopup) (spinnerPopup);
+                final int popupWidth = dropdownPopup.getWidth();
+                final int spinnerWidth = spinner.getWidth();
+
+                computedOffset = location[0] + (spinnerWidth - popupWidth - offset);
+            } else {
+                computedOffset = offset;
+            }
+        }
+
+        onView(withText(ONE)).check(matches(
+                hasOffset(
+                        computedOffset,
+                        isVerticalOffset ? "has vertical offset" : "has horizontal offset",
+                        isVerticalOffset)
+        ));
+    }
+
     private ViewAction slowScrollPopup() {
         return new GeneralSwipeAction(Swipe.SLOW,
                 new CoordinatesProvider() {
@@ -314,4 +379,50 @@
             }
         });
     }
+
+    private Matcher<View> hasOffset(
+            final int offset,
+            final String desc,
+            final boolean isVerticalOffset) {
+        return new TypeSafeMatcher<View>() {
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText(desc);
+            }
+
+            @Override
+            protected boolean matchesSafely(View view) {
+                if (view.getParent() instanceof DropDownListView) {
+                    final DropDownListView dropDownListView = (DropDownListView) (view.getParent());
+                    int[] location = new int[2];
+                    dropDownListView.getLocationOnScreen(location);
+                    dropDownListView.getWidth();
+                    return location[isVerticalOffset ? 1 : 0] == offset;
+                }
+
+                return false;
+            }
+        };
+    }
+
+    private void setRtl() {
+        final Context context = mInstrumentation.getTargetContext();
+
+        mActivity.finish();
+        final Intent intent = new Intent(context, AppCompatSpinnerActivity.class);
+        intent.putExtra("language", LocaleTestUtils.RTL_LANGUAGE);
+
+        Instrumentation.ActivityMonitor monitor =
+                new Instrumentation.ActivityMonitor(mActivity.getClass().getName(), null, false);
+        mInstrumentation.addMonitor(monitor);
+
+        mActivity = mActivityTestRule.launchActivity(intent);
+
+        mInstrumentation.waitForIdleSync();
+        mInstrumentation.waitForMonitor(monitor);
+
+        mContainer = mActivity.findViewById(R.id.container);
+        mResources = mActivity.getResources();
+    }
 }
diff --git a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
index 534048e..2378f9b 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
@@ -116,6 +116,15 @@
             android:entries="@array/numbers_array"
             android:focusable="false"
             android:spinnerMode="dropdown" />
+
+        <androidx.appcompat.widget.AppCompatSpinner
+            android:id="@+id/spinner_dropdown_popup_small"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:entries="@array/numbers_array_small"
+            android:popupBackground="@null"
+            android:focusable="false"
+            android:spinnerMode="dropdown" />
     </LinearLayout>
 
 </ScrollView>
diff --git a/appcompat/src/androidTest/res/values/strings.xml b/appcompat/src/androidTest/res/values/strings.xml
index 0188024..9b0c971 100644
--- a/appcompat/src/androidTest/res/values/strings.xml
+++ b/appcompat/src/androidTest/res/values/strings.xml
@@ -120,6 +120,11 @@
         <item>38</item>
         <item>39</item>
     </string-array>
+    <string-array name="numbers_array_small">
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+    </string-array>
 
     <string name="night_mode">DAY</string>
 
diff --git a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
index 1033e46..01d7375 100644
--- a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
+++ b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
@@ -347,6 +347,7 @@
     @Override
     public void setDropDownHorizontalOffset(int pixels) {
         if (mPopup != null) {
+            mPopup.setHorizontalOriginalOffset(pixels);
             mPopup.setHorizontalOffset(pixels);
         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
             super.setDropDownHorizontalOffset(pixels);
@@ -836,6 +837,8 @@
         void setBackgroundDrawable(Drawable bg);
         void setVerticalOffset(int px);
         void setHorizontalOffset(int px);
+        void setHorizontalOriginalOffset(int px);
+        int getHorizontalOriginalOffset();
         Drawable getBackground();
         int getVerticalOffset();
         int getHorizontalOffset();
@@ -933,12 +936,24 @@
         public int getHorizontalOffset() {
             return 0;
         }
+
+        @Override
+        public void setHorizontalOriginalOffset(int px) {
+            Log.e(TAG, "Cannot set horizontal (original) offset for MODE_DIALOG, ignoring");
+        }
+
+        @Override
+        public int getHorizontalOriginalOffset() {
+            return 0;
+        }
     }
 
-    private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
+    @VisibleForTesting
+    class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
         private CharSequence mHintText;
         ListAdapter mAdapter;
         private final Rect mVisibleRect = new Rect();
+        private int mOriginalHorizontalOffset;
 
         public DropdownPopup(Context context, AttributeSet attrs, int defStyleAttr) {
             super(context, attrs, defStyleAttr);
@@ -1007,9 +1022,10 @@
                 setContentWidth(mDropDownWidth);
             }
             if (ViewUtils.isLayoutRtl(AppCompatSpinner.this)) {
-                hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
+                hOffset += spinnerWidth - spinnerPaddingRight - getWidth()
+                        - getHorizontalOriginalOffset();
             } else {
-                hOffset += spinnerPaddingLeft;
+                hOffset += spinnerPaddingLeft + getHorizontalOriginalOffset();
             }
             setHorizontalOffset(hOffset);
         }
@@ -1075,5 +1091,15 @@
         boolean isVisibleToUser(View view) {
             return ViewCompat.isAttachedToWindow(view) && view.getGlobalVisibleRect(mVisibleRect);
         }
+
+        @Override
+        public void setHorizontalOriginalOffset(int px) {
+            mOriginalHorizontalOffset = px;
+        }
+
+        @Override
+        public int getHorizontalOriginalOffset() {
+            return mOriginalHorizontalOffset;
+        }
     }
 }
diff --git a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
index b52de5c..e2a8f9b 100644
--- a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
+++ b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
@@ -719,25 +719,31 @@
     @RequiresApi(23)
     private StaticLayout createStaticLayoutForMeasuring(CharSequence text,
             Layout.Alignment alignment, int availableWidth, int maxLines) {
-        // Can use the StaticLayout.Builder (along with TextView params added in or after
-        // API 23) to construct the layout.
-        final TextDirectionHeuristic textDirectionHeuristic = invokeAndReturnWithDefault(
-                mTextView, "getTextDirectionHeuristic",
-                TextDirectionHeuristics.FIRSTSTRONG_LTR);
 
         final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
                 text, 0, text.length(),  mTempTextPaint, availableWidth);
 
-        return layoutBuilder.setAlignment(alignment)
+        layoutBuilder.setAlignment(alignment)
                 .setLineSpacing(
                         mTextView.getLineSpacingExtra(),
                         mTextView.getLineSpacingMultiplier())
                 .setIncludePad(mTextView.getIncludeFontPadding())
                 .setBreakStrategy(mTextView.getBreakStrategy())
                 .setHyphenationFrequency(mTextView.getHyphenationFrequency())
-                .setMaxLines(maxLines == -1 ? Integer.MAX_VALUE : maxLines)
-                .setTextDirection(textDirectionHeuristic)
-                .build();
+                .setMaxLines(maxLines == -1 ? Integer.MAX_VALUE : maxLines);
+
+        try {
+            // Can use the StaticLayout.Builder (along with TextView params added in or after
+            // API 23) to construct the layout.
+            final TextDirectionHeuristic textDirectionHeuristic = invokeAndReturnWithDefault(
+                    mTextView, "getTextDirectionHeuristic",
+                    TextDirectionHeuristics.FIRSTSTRONG_LTR);
+            layoutBuilder.setTextDirection(textDirectionHeuristic);
+        } catch (ClassCastException e) {
+            // On some devices this exception happens, details: b/127137059.
+            Log.w(TAG, "Failed to obtain TextDirectionHeuristic, auto size may be incorrect");
+        }
+        return layoutBuilder.build();
     }
 
     @RequiresApi(16)
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/Adb.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/Adb.kt
index 05df30a..a75b4f4 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/Adb.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/Adb.kt
@@ -42,13 +42,16 @@
     fun execSync(
         adbCmd: String,
         deviceId: String? = null,
-        shouldThrow: Boolean = true
+        shouldThrow: Boolean = true,
+        silent: Boolean = false
     ): ProcessResult {
         val subCmd = adbCmd.trim().split(Regex("\\s+")).toTypedArray()
         val adbArgs = if (!deviceId.isNullOrEmpty()) arrayOf("-s", deviceId) else emptyArray()
         val cmd = arrayOf(adbPath, *adbArgs, *subCmd)
 
-        logger.log(LogLevel.INFO, cmd.joinToString(" "))
+        if (!silent) {
+            logger.log(LogLevel.INFO, cmd.joinToString(" "))
+        }
         val process = Runtime.getRuntime().exec(cmd)
 
         if (!process.waitFor(5, TimeUnit.SECONDS)) {
@@ -58,8 +61,13 @@
         val stdout = process.inputStream.bufferedReader().use { it.readText() }
         val stderr = process.errorStream.bufferedReader().use { it.readText() }
 
-        logger.log(LogLevel.QUIET, stdout)
-        logger.log(LogLevel.WARN, stderr)
+        if (!stdout.isBlank() && !silent) {
+            logger.log(LogLevel.QUIET, stdout.trim())
+        }
+
+        if (!stderr.isBlank() && shouldThrow && !silent) {
+            logger.log(LogLevel.ERROR, stderr.trim())
+        }
 
         if (shouldThrow && process.exitValue() != 0) {
             throw GradleException(stderr)
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/LockClocksTask.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/LockClocksTask.kt
index c65034d..b006f02 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/LockClocksTask.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/LockClocksTask.kt
@@ -35,7 +35,7 @@
 
         // Skip "adb root" if already rooted as it will fail.
         if (adb.execSync("shell su exit", shouldThrow = false).exitValue != 0) {
-            adb.execSync("root")
+            adb.execSync("root", silent = true)
         }
 
         val dest = "/data/local/tmp/lockClocks.sh"
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
index b22634d..89e24428 100755
--- a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
@@ -137,8 +137,10 @@
       echo 0 > ${CPU_BASE}/cpu${cpu}/online
     done
 
-    echo "\nLocked CPUs ${enableIndices// /,} to $chosenFreq / $maxFreq KHz"
+    echo "=================================="
+    echo "Locked CPUs ${enableIndices// /,} to $chosenFreq / $maxFreq KHz"
     echo "Disabled CPUs ${disableIndices// /,}"
+    echo "=================================="
 }
 
 # If we have a Qualcomm GPU, find its max frequency, and lock to
@@ -146,12 +148,12 @@
 function_lock_gpu_kgsl() {
     if [ ! -d /sys/class/kgsl/kgsl-3d0/ ]; then
         # not kgsl, abort
-        echo "\nCurrently don't support locking GPU clocks of $MODEL ($DEVICE)"
+        echo "Currently don't support locking GPU clocks of $MODEL ($DEVICE)"
         return -1
     fi
     if [ ${DEVICE} == "walleye" ] || [ ${DEVICE} == "taimen" ]; then
         # Workaround crash
-        echo "\nUnable to lock GPU clocks of $MODEL ($DEVICE)"
+        echo "Unable to lock GPU clocks of $MODEL ($DEVICE)"
         return -1
     fi
 
@@ -218,7 +220,7 @@
         echo "index = $chosenIndex"
         exit -1
     fi
-    echo "\nLocked GPU to $chosenFreq / $gpuMaxFreq Hz"
+    echo "Locked GPU to $chosenFreq / $gpuMaxFreq Hz"
 }
 
 # kill processes that manage thermals / scaling
@@ -235,7 +237,7 @@
 if [ ${DEVICE} == "marlin" ] || [ ${DEVICE} == "sailfish" ]; then
     echo 13763 > /sys/class/devfreq/soc:qcom,gpubw/max_freq
 else
-    echo "\nUnable to lock memory bus of $MODEL ($DEVICE)."
+    echo "Unable to lock memory bus of $MODEL ($DEVICE)."
 fi
 
-echo "\n$DEVICE clocks have been locked - to reset, reboot the device\n"
+echo "$DEVICE clocks have been locked - to reset, reboot the device"
diff --git a/benchmark/gradle-plugin/src/test/kotlin/androidx/benchmark/gradle/BenchmarkPluginTest.kt b/benchmark/gradle-plugin/src/test/kotlin/androidx/benchmark/gradle/BenchmarkPluginTest.kt
index ab22d01..13e2683 100644
--- a/benchmark/gradle-plugin/src/test/kotlin/androidx/benchmark/gradle/BenchmarkPluginTest.kt
+++ b/benchmark/gradle-plugin/src/test/kotlin/androidx/benchmark/gradle/BenchmarkPluginTest.kt
@@ -18,7 +18,6 @@
 
 import org.gradle.testkit.runner.GradleRunner
 import org.gradle.testkit.runner.UnexpectedBuildFailure
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -78,10 +77,6 @@
             .withPluginClasspath()
     }
 
-    @After
-    fun tearDown() {
-    }
-
     @Test
     fun applyPluginAppProject() {
         buildFile.writeText(
diff --git a/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.kt b/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.kt
index 97ea30b..32df3ab 100644
--- a/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.kt
+++ b/benchmark/src/main/java/androidx/benchmark/BenchmarkRule.kt
@@ -151,55 +151,52 @@
     override fun apply(base: Statement, description: Description): Statement {
         return RuleChain
             .outerRule(GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE))
-            .around { base, description ->
-                object : Statement() {
-                    @Throws(Throwable::class)
-                    override fun evaluate() {
-                        applied = true
-                        var invokeMethodName = description.methodName
-                        Log.i(TAG, "Running ${description.className}#$invokeMethodName")
-
-                        // validate and simplify the function name.
-                        // First, remove the "test" prefix which normally comes from CTS test.
-                        // Then make sure the [subTestName] is valid, not just numbers like [0].
-                        if (invokeMethodName.startsWith("test")) {
-                            assertTrue(
-                                "The test name $invokeMethodName is too short",
-                                invokeMethodName.length > 5
-                            )
-                            invokeMethodName = invokeMethodName.substring(4, 5).toLowerCase() +
-                                    invokeMethodName.substring(5)
-                        }
-
-                        val index = invokeMethodName.lastIndexOf('[')
-                        if (index > 0) {
-                            val allDigits =
-                                invokeMethodName.substring(index + 1, invokeMethodName.length - 1)
-                                    .all { Character.isDigit(it) }
-                            assertFalse(
-                                "The name in [] can't contain only digits for $invokeMethodName",
-                                allDigits
-                            )
-                        }
-
-                        base.evaluate()
-
-                        val fullTestName = WarningState.WARNING_PREFIX +
-                                description.testClass.simpleName + "." + invokeMethodName
-                        internalState.sendStatus(fullTestName)
-
-                        ResultWriter.appendStats(
-                            internalState.getReport(
-                                testName = WarningState.WARNING_PREFIX + invokeMethodName,
-                                className = description.className
-                            )
-                        )
-                    }
-                }
-            }
+            .around(::applyInternal)
             .apply(base, description)
     }
 
+    private fun applyInternal(base: Statement, description: Description) = Statement {
+        applied = true
+        var invokeMethodName = description.methodName
+        Log.i(TAG, "Running ${description.className}#$invokeMethodName")
+
+        // validate and simplify the function name.
+        // First, remove the "test" prefix which normally comes from CTS test.
+        // Then make sure the [subTestName] is valid, not just numbers like [0].
+        if (invokeMethodName.startsWith("test")) {
+            assertTrue(
+                "The test name $invokeMethodName is too short",
+                invokeMethodName.length > 5
+            )
+            invokeMethodName = invokeMethodName.substring(4, 5).toLowerCase() +
+                    invokeMethodName.substring(5)
+        }
+
+        val index = invokeMethodName.lastIndexOf('[')
+        if (index > 0) {
+            val allDigits =
+                invokeMethodName.substring(index + 1, invokeMethodName.length - 1)
+                    .all { Character.isDigit(it) }
+            assertFalse(
+                "The name in [] can't contain only digits for $invokeMethodName",
+                allDigits
+            )
+        }
+
+        base.evaluate()
+
+        val fullTestName = WarningState.WARNING_PREFIX +
+                description.testClass.simpleName + "." + invokeMethodName
+        internalState.sendStatus(fullTestName)
+
+        ResultWriter.appendStats(
+            internalState.getReport(
+                testName = WarningState.WARNING_PREFIX + invokeMethodName,
+                className = description.className
+            )
+        )
+    }
+
     internal companion object {
         private const val TAG = "BenchmarkRule"
     }
@@ -235,3 +232,7 @@
         block(localScope)
     }
 }
+
+internal fun Statement(evaluate: () -> Unit) = object : Statement() {
+    override fun evaluate() = evaluate()
+}
\ No newline at end of file
diff --git a/browser/build.gradle b/browser/build.gradle
index f42d8cd..a9f7221 100644
--- a/browser/build.gradle
+++ b/browser/build.gradle
@@ -17,7 +17,7 @@
     api(project(":core"))
     api("androidx.annotation:annotation:1.1.0-rc01")
     api(project(":interpolator"))
-    api(project(":collection"))
+    api("androidx.collection:collection:1.1.0-rc01")
     api(project(":legacy-support-core-ui"))
 
     implementation(project(":concurrent:concurrent-futures"))
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index 9df5551..da633e8 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -21,7 +21,7 @@
     build_versions.kotlin = kotlin_override
     logger.warn("USING OVERRIDDEN KOTLIN GRADLE PLUGIN DEPENDENCY " + build_versions.kotlin)
 } else {
-    build_versions.kotlin = '1.3.20'
+    build_versions.kotlin = '1.3.31'
 }
 build_versions.lint = '26.4.0'
 build_versions.dokka = '0.9.17-g002'
diff --git a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
index 4b3e854..ea5a7ce 100644
--- a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
@@ -18,15 +18,12 @@
 
 import androidx.build.Strategy.Prebuilts
 import androidx.build.Strategy.TipOfTree
-import androidx.build.checkapi.ApiXmlConversionTask
-import androidx.build.checkapi.CheckApiTasks
 import androidx.build.doclava.ChecksConfig
 import androidx.build.doclava.DEFAULT_DOCLAVA_CONFIG
 import androidx.build.doclava.DoclavaTask
 import androidx.build.docs.ConcatenateFilesTask
 import androidx.build.docs.GenerateDocsTask
 import androidx.build.gradle.isRoot
-import androidx.build.jdiff.JDiffTask
 import com.android.build.gradle.AppExtension
 import com.android.build.gradle.LibraryExtension
 import com.android.build.gradle.api.BaseVariant
@@ -48,7 +45,6 @@
 import org.gradle.api.tasks.javadoc.Javadoc
 import org.gradle.api.tasks.util.PatternSet
 import java.io.File
-import java.lang.IllegalStateException
 import java.net.URLClassLoader
 import java.time.LocalDateTime
 import java.time.format.DateTimeFormatter
@@ -57,10 +53,6 @@
 
 private const val DOCLAVA_DEPENDENCY = "com.android:doclava:1.0.6"
 
-private const val JDIFF_DEPENDENCY = "com.android:jdiff:1.1.0"
-private const val XML_PARSER_APIS_DEPENDENCY = "xerces:xmlParserAPIs:2.6.2"
-private const val XERCES_IMPL_DEPENDENCY = "xerces:xercesImpl:2.6.2"
-
 data class DacOptions(val libraryroot: String, val dataname: String)
 
 class DiffAndDocs private constructor(
@@ -76,7 +68,6 @@
     private val docsTasks: MutableMap<String, TaskProvider<GenerateDocsTask>> = mutableMapOf()
     private val aggregateOldApiTxtsTask: TaskProvider<ConcatenateFilesTask>
     private val aggregateNewApiTxtsTask: TaskProvider<ConcatenateFilesTask>
-    private val generateDiffsTask: TaskProvider<JDiffTask>
 
     init {
         val doclavaConfiguration = root.configurations.create("doclava")
@@ -126,55 +117,17 @@
             task.dependsOn(docsTasks[TIP_OF_TREE.name])
         }
 
-        val docletClasspath = doclavaConfiguration.resolve()
         val oldOutputTxt = File(root.docsDir(), "previous.txt")
         aggregateOldApiTxtsTask = root.tasks.register("aggregateOldApiTxts",
             ConcatenateFilesTask::class.java) {
             it.Output = oldOutputTxt
         }
 
-        val oldApisTask = root.tasks.register("oldApisXml",
-            ApiXmlConversionTask::class.java) {
-            it.classpath = root.files(docletClasspath)
-            it.dependsOn(doclavaConfiguration)
-
-            it.inputApiFile = oldOutputTxt
-            it.dependsOn(aggregateOldApiTxtsTask)
-
-            it.outputApiXmlFile = File(root.docsDir(), "previous.xml")
-        }
-
         val newApiTxt = File(root.docsDir(), newVersion)
         aggregateNewApiTxtsTask = root.tasks.register("aggregateNewApiTxts",
             ConcatenateFilesTask::class.java) {
             it.Output = newApiTxt
         }
-
-        val newApisTask = root.tasks.register("newApisXml",
-            ApiXmlConversionTask::class.java) {
-            it.classpath = root.files(docletClasspath)
-
-            it.inputApiFile = newApiTxt
-            it.dependsOn(aggregateNewApiTxtsTask)
-
-            it.outputApiXmlFile = File(root.docsDir(), "$newVersion.xml")
-        }
-
-        val jdiffConfiguration = root.configurations.create("jdiff")
-        jdiffConfiguration.dependencies.add(root.dependencies.create(JDIFF_DEPENDENCY))
-        jdiffConfiguration.dependencies.add(root.dependencies.create(XML_PARSER_APIS_DEPENDENCY))
-        jdiffConfiguration.dependencies.add(root.dependencies.create(XERCES_IMPL_DEPENDENCY))
-
-        generateDiffsTask = createGenerateDiffsTask(root,
-            oldApisTask,
-            newApisTask,
-            jdiffConfiguration)
-
-        generateDiffsTask.configure { diffTask ->
-            docsTasks.values.forEach { docs ->
-                diffTask.dependsOn(docs)
-            }
-        }
     }
 
     companion object {
@@ -323,8 +276,6 @@
         tipOfTreeTasks(extension) { task ->
             registerJavaProjectForDocsTask(task, compileJava)
         }
-
-        registerJavaProjectForDocsTask(generateDiffsTask, compileJava)
     }
 
     /**
@@ -354,40 +305,6 @@
             }
         }
     }
-
-    private fun setupApiVersioningInDocsTasks(
-        extension: AndroidXExtension,
-        checkApiTasks: CheckApiTasks
-    ) {
-        rules.forEach { rules ->
-            val project = extension.project
-            val strategy = rules.resolve(extension)?.strategy
-            val version = if (strategy is Prebuilts) {
-                strategy.version
-            } else {
-                extension.project.version()
-            }
-            docsTasks[rules.name]!!.configure { docs ->
-                // Track API change history.
-                docs.addSinceFilesFrom(project.projectDir)
-                // Associate current API surface with the Maven artifact.
-                val artifact = "${project.group}:${project.name}:$version"
-                docs.addArtifact(checkApiTasks.generateApi.get().apiFile!!.absolutePath, artifact)
-                docs.dependsOn(checkApiTasks.generateApi)
-            }
-        }
-    }
-
-    private fun addCheckApiTasksToGraph(tasks: CheckApiTasks) {
-        docsTasks.values.forEach { docs ->
-            docs.configure {
-                it.dependsOn(tasks.generateApi)
-            }
-        }
-        anchorTask.configure {
-            it.dependsOn(tasks.checkApi)
-        }
-    }
 }
 
 /**
@@ -442,68 +359,6 @@
     }
 }
 
-/**
- * Generates API diffs.
- * <p>
- * By default, diffs are generated for the delta between current.txt and the
- * next most recent X.Y.Z.txt API file. Behavior may be changed by specifying
- * one or both of -PtoApi and -PfromApi.
- * <p>
- * If both fromApi and toApi are specified, diffs will be generated for
- * fromApi -> toApi. For example, 25.0.0 -> 26.0.0 diffs could be generated by
- * using:
- * <br><code>
- *   ./gradlew generateDiffs -PfromApi=25.0.0 -PtoApi=26.0.0
- * </code>
- * <p>
- * If only toApi is specified, it MUST be specified as X.Y.Z and diffs will be
- * generated for (release before toApi) -> toApi. For example, 24.2.0 -> 25.0.0
- * diffs could be generated by using:
- * <br><code>
- *   ./gradlew generateDiffs -PtoApi=25.0.0
- * </code>
- * <p>
- * If only fromApi is specified, diffs will be generated for fromApi -> current.
- * For example, lastApiReview -> current diffs could be generated by using:
- * <br><code>
- *   ./gradlew generateDiffs -PfromApi=lastApiReview
- * </code>
- * <p>
- */
-private fun createGenerateDiffsTask(
-    project: Project,
-    oldApiTask: TaskProvider<ApiXmlConversionTask>,
-    newApiTask: TaskProvider<ApiXmlConversionTask>,
-    jdiffConfig: Configuration
-): TaskProvider<JDiffTask> =
-        project.tasks.register("generateDiffs", JDiffTask::class.java) {
-            it.apply {
-                // Base classpath is Android SDK, sub-projects add their own.
-                classpath = androidJarFile(project)
-
-                // JDiff properties.
-                oldApiXmlFile = oldApiTask.get().outputApiXmlFile
-                newApiXmlFile = newApiTask.get().outputApiXmlFile
-
-                val newApi = newApiXmlFile.name.substringBeforeLast('.')
-                val docsDir = File(project.rootProject.docsDir(), "public")
-
-                newJavadocPrefix = "../../../../../reference/"
-                destinationDir = File(docsDir,
-                        "online/sdk/support_api_diff/${project.name}/$newApi")
-
-                // Javadoc properties.
-                docletpath = jdiffConfig.resolve()
-                title = "Support&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;Report"
-
-                exclude("**/R.java")
-                dependsOn(oldApiTask, newApiTask, jdiffConfig)
-                doLast {
-                    project.logger.lifecycle("generated diffs into $destinationDir")
-                }
-            }
-        }
-
 // Generates a distribution artifact for online docs.
 private fun createDistDocsTask(
     project: Project,
@@ -616,18 +471,6 @@
             }
         }
 
-private fun createGenerateLocalApiDiffsArchiveTask(
-    project: Project,
-    diffTask: TaskProvider<JDiffTask>
-): TaskProvider<Zip> = project.tasks.register("generateLocalApiDiffsArchive", Zip::class.java) {
-    val docsDir = project.rootProject.docsDir()
-    it.from(diffTask.map {
-        it.destinationDir
-    })
-    it.destinationDirectory.set(File(docsDir, "online/sdk/support_api_diff/${project.name}"))
-    it.to("${project.version}.zip")
-}
-
 private fun sdkApiFile(project: Project) = File(project.docsDir(), "release/sdk_current.txt")
 
 fun <T : Task> TaskContainer.createWithConfig(
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index cea017c..8a0866b 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -37,7 +37,7 @@
     val CAR_CLUSTER = Version("1.0.0-alpha6")
     val CAR_MODERATOR = Version("1.0.0-alpha1")
     val CARDVIEW = Version("1.1.0-alpha01")
-    val COLLECTION = Version("1.1.0-rc01")
+    val COLLECTION = Version("1.2.0-alpha01")
     val CONTENTPAGER = Version("1.1.0-alpha01")
     val COMPOSE = Version("1.0.0-alpha01")
     val COORDINATORLAYOUT = Version("1.1.0-alpha02")
diff --git a/buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt b/buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt
deleted file mode 100644
index b191d56..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/checkapi/ApiXmlConversionTask.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.build.checkapi
-
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.JavaExec
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-import java.io.File
-
-/**
- * Task that converts the given API txt file to XML format.
- */
-open class ApiXmlConversionTask : JavaExec() {
-    @Optional
-    @InputFile
-    var inputApiFile: File? = null
-
-    @get:OutputFile
-    lateinit var outputApiXmlFile: File
-
-    init {
-        maxHeapSize = "1024m"
-
-        // Despite this tool living in ApiCheck, its purpose more fits with doclava's "purposes",
-        // generation of api files in this case. Thus, I am putting this in the doclava package.
-        main = "com.google.doclava.apicheck.ApiCheck"
-    }
-
-    override fun exec() {
-        val input = inputApiFile
-        if (input != null) {
-            args = listOf("-convert2xml", input.absolutePath, outputApiXmlFile.absolutePath)
-            super.exec()
-        } else {
-            outputApiXmlFile.delete()
-            outputApiXmlFile.writeText("<api>\n</api>")
-        }
-    }
-}
diff --git a/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt b/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt
index a304e46..f196311 100644
--- a/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt
@@ -18,221 +18,16 @@
 
 import androidx.build.AndroidXExtension
 import androidx.build.Version
-import androidx.build.androidJarFile
-import androidx.build.doclava.CHECK_API_CONFIG_DEVELOP
-import androidx.build.doclava.CHECK_API_CONFIG_PATCH
-import androidx.build.doclava.CHECK_API_CONFIG_RELEASE
-import androidx.build.doclava.ChecksConfig
-import androidx.build.doclava.DoclavaTask
-import androidx.build.docs.ConcatenateFilesTask
-import androidx.build.docsDir
-import androidx.build.jdiff.JDiffTask
-import androidx.build.processProperty
 import androidx.build.version
 import org.gradle.api.GradleException
 import org.gradle.api.Project
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.plugins.JavaBasePlugin
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.TaskProvider
 import java.io.File
 
-data class CheckApiTasks(
-    val generateApi: TaskProvider<DoclavaTask>,
-    val checkApi: TaskProvider<CheckApiTask>,
-    val generateLocalDiffs: TaskProvider<JDiffTask>
-)
-
 enum class ApiType {
     CLASSAPI,
     RESOURCEAPI
 }
 
-/**
- * Sets up api tasks for the given project
- */
-fun initializeApiChecksForProject(
-    project: Project,
-    aggregateOldApiTxtsTask: TaskProvider<ConcatenateFilesTask>,
-    aggregateNewApiTxtsTask: TaskProvider<ConcatenateFilesTask>
-): CheckApiTasks {
-    if (!project.hasProperty("docsDir")) {
-        project.extensions.add("docsDir", File(project.rootProject.docsDir(), project.name))
-    }
-    val version = project.version()
-
-    val doclavaConfiguration = project.rootProject.configurations.getByName("doclava")
-    val docletClasspath = doclavaConfiguration.resolve()
-    val generateApi = createGenerateApiTask(project, docletClasspath)
-    generateApi.configure {
-        it.dependsOn(doclavaConfiguration)
-    }
-
-    // for verifying that the API surface has not broken since the last minor release
-    val lastReleasedApiFile = project.getRequiredCompatibilityApiFile()
-
-    val whitelistFile = lastReleasedApiFile?.let { apiFile ->
-        File(lastReleasedApiFile.parentFile, stripExtension(apiFile.name) + ".ignore")
-    }
-    val checkApiRelease = createCheckApiTask(project,
-            "checkApiRelease",
-            docletClasspath,
-            CHECK_API_CONFIG_RELEASE,
-            lastReleasedApiFile,
-            generateApi.map {
-                it.apiFile!!
-            },
-            whitelistFile)
-    checkApiRelease.configure {
-        it.dependsOn(generateApi)
-    }
-
-    // Allow a comma-delimited list of whitelisted errors.
-    if (project.hasProperty("ignore")) {
-        checkApiRelease.configure {
-            it.whitelistErrors = (project.properties["ignore"] as String)
-                    .split(',').toSet()
-        }
-    }
-
-    // Check whether the development API surface has changed.
-    val verifyConfig = if (version.isPatch()) CHECK_API_CONFIG_PATCH else CHECK_API_CONFIG_DEVELOP
-    val currentApiFile = project.getCurrentApiFile()
-    val checkApi = createCheckApiTask(project,
-            "checkApi",
-            docletClasspath,
-            verifyConfig,
-            currentApiFile,
-            generateApi.map {
-                it.apiFile!!
-            },
-            null)
-    checkApi.configure {
-        it.dependsOn(generateApi, checkApiRelease)
-        it.group = JavaBasePlugin.VERIFICATION_GROUP
-        it.description = "Verify the API surface."
-    }
-
-    val updateApiTask = createUpdateApiTask(project, checkApiRelease)
-    updateApiTask.configure {
-        it.dependsOn(checkApiRelease)
-    }
-
-    val oldApiTxt = getOldApiTxtForDocDiffs(project)
-    if (oldApiTxt != null) {
-        aggregateOldApiTxtsTask.configure {
-            it.addInput(project.name, oldApiTxt)
-        }
-    }
-    val newApiTxtProvider = getNewApiTxt(project, generateApi)
-    aggregateNewApiTxtsTask.configure {
-        it.inputs.file(newApiTxtProvider.file)
-        it.addInput(project.name, newApiTxtProvider.file.get())
-    }
-
-    val newApiTask = createNewApiXmlTask(project, generateApi, doclavaConfiguration)
-    val oldApiTask = createOldApiXml(project, doclavaConfiguration)
-
-    val jdiffConfiguration = project.rootProject.configurations.getByName("jdiff")
-
-    val generatelocalDiffsTask = createGenerateLocalDiffsTask(project,
-            oldApiTask,
-            newApiTask,
-            jdiffConfiguration)
-
-    return CheckApiTasks(generateApi, checkApi, generatelocalDiffsTask)
-}
-
-private fun createGenerateLocalDiffsTask(
-    project: Project,
-    oldApiTask: TaskProvider<ApiXmlConversionTask>,
-    newApiTask: TaskProvider<ApiXmlConversionTask>,
-    jdiffConfig: Configuration
-): TaskProvider<JDiffTask> =
-        project.tasks.register("generateLocalDiffs", JDiffTask::class.java) {
-            it.apply {
-                // Base classpath is Android SDK, sub-projects add their own.
-                classpath = androidJarFile(project)
-
-                // JDiff properties.
-                oldApiXmlFile = oldApiTask.get().outputApiXmlFile
-                newApiXmlFile = newApiTask.get().outputApiXmlFile
-
-                val newApi = project.processProperty("toApi") ?: project.version
-                val docsDir = project.rootProject.docsDir()
-
-                newJavadocPrefix = "../../../../../reference/"
-                destinationDir = File(docsDir,
-                        "online/sdk/support_api_diff/${project.name}/$newApi")
-                // Javadoc properties.
-                docletpath = jdiffConfig.resolve()
-                title = "AndroidX&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;Report"
-
-                exclude("**/BuildConfig.java", "**/R.java")
-                dependsOn(oldApiTask, newApiTask, jdiffConfig)
-                doFirst {
-                    println("Generating diffs from api version " +
-                            "${stripExtension(oldApiTask.get().outputApiXmlFile.name)} " +
-                            "to api version $newApi")
-                }
-            }
-        }
-
-/**
- * Converts the <code>fromApi</code>.txt file (or the most recently released
- * X.Y.Z.txt if not explicitly defined using -PfromAPi=<file>) to XML format
- * for use by JDiff.
- */
-private fun createOldApiXml(project: Project, doclavaConfig: Configuration) =
-        project.tasks.register("oldApiXml", ApiXmlConversionTask::class.java) {
-            it.apply {
-                val fromApi = project.processProperty("fromApi")
-                classpath = project.files(doclavaConfig.resolve())
-                val rootFolder = project.projectDir
-                inputApiFile = if (fromApi != null) {
-                    // Use an explicit API file.
-                    File(rootFolder, "api/$fromApi.txt")
-                } else {
-                    // Use the most recently released API file.
-                    getLastReleasedApiFile(rootFolder, Version(project.processProperty("toApi")
-                        ?: project.version.toString()), false, false)
-                }
-
-                outputApiXmlFile = File(project.docsDir(),
-                    "release/${stripExtension(inputApiFile?.name ?: "creation")}.xml")
-
-                dependsOn(doclavaConfig)
-            }
-        }
-
-/**
- * Converts the <code>toApi</code>.txt file (or current.txt if not explicitly
- * defined using -PtoApi=<file>) to XML format for use by JDiff.
- */
-private fun createNewApiXmlTask(
-    project: Project,
-    generateApi: TaskProvider<DoclavaTask>,
-    doclavaConfig: Configuration
-) =
-        project.tasks.register("newApiXml", ApiXmlConversionTask::class.java) {
-            it.apply {
-                classpath = project.files(doclavaConfig.resolve())
-                val toApi = project.processProperty("toApi")
-
-                if (toApi != null && toApi != project.version) {
-                    // Use an explicit API file.
-                    inputApiFile = File(project.projectDir, "api/$toApi.txt")
-                } else {
-                    // Use the current API file (e.g. current.txt).
-                    inputApiFile = generateApi.get().apiFile!!
-                    dependsOn(generateApi, doclavaConfig)
-                }
-                // Use either the toApi version, otherwise the most recent version.
-                outputApiXmlFile = File(project.docsDir(),
-                    "release/${toApi ?: project.version}.xml")
-            }
-        }
-
 fun Project.hasApiFolder() = File(projectDir, "api").exists()
 
 fun hasApiTasks(project: Project, extension: AndroidXExtension): Boolean {
@@ -259,53 +54,6 @@
     return false
 }
 
-// Creates a new task on the project for generating API files
-private fun createGenerateApiTask(project: Project, docletpathParam: Collection<File>) =
-        project.tasks.register("generateApi", DoclavaTask::class.java) {
-            it.apply {
-                // Base classpath is Android SDK, sub-projects add their own.
-                classpath = androidJarFile(project)
-                apiFile = File(project.docsDir(), "release/${project.name}/current.txt")
-                setDocletpath(docletpathParam)
-                destinationDir = project.docsDir()
-                generateDocs = false
-
-                coreJavadocOptions {
-                    addBooleanOption("stubsourceonly", true)
-                }
-
-                exclude("**/R.java")
-            }
-        }
-
-/**
- * Constructs a new task to copy a generated API file to an appropriately-named "official" API file
- * suitable for source control. This task should be called prior to source control check-in whenever
- * the public API has been modified.
- * <p>
- * The output API file varies according to version:
- * <ul>
- * <li>Snapshot and pre-release versions (e.g. X.Y.Z-SNAPSHOT, X.Y.Z-alphaN) output to current.txt
- * <li>Release versions (e.g. X.Y.Z) output to X.Y.0.txt, throwing an exception if the API has been
- *     finalized and the file already exists
- * </ul>
- */
-private fun createUpdateApiTask(project: Project, checkApiRelease: TaskProvider<CheckApiTask>) =
-        project.tasks.register("updateApi", UpdateApiTask::class.java) {
-            it.apply {
-                group = JavaBasePlugin.VERIFICATION_GROUP
-                description = "Updates the candidate API file to incorporate valid changes."
-                newApiFile = checkApiRelease.get().newApiFile
-                oldApiFile = project.getCurrentApiFile()
-                whitelistErrors = checkApiRelease.get().whitelistErrors
-                whitelistErrorsFile = checkApiRelease.get().whitelistErrorsFile
-                doFirst {
-                    // Replace the expected whitelist with the detected whitelist.
-                    whitelistErrors = checkApiRelease.get().detectedWhitelistErrors
-                }
-            }
-        }
-
 /**
  * Returns the API file whose contents match the project's source code.
  * This is the API file that the updateApi task will write to.
@@ -362,24 +110,6 @@
     return File(apiDir, "${version.major}.${version.minor}.0$extra.txt")
 }
 
-/**
- * Returns the filepath of the previous API txt file
- */
-private fun getOldApiTxtForDocDiffs(project: Project): File? {
-    val toApi = project.processProperty("toApi")?.let {
-        Version.parseOrNull(it)
-    }
-    val fromApi = project.processProperty("fromApi")
-    val rootFolder = project.projectDir
-    return if (fromApi != null) {
-        // Use an explicit API file.
-        File(rootFolder, "api/$fromApi.txt")
-    } else {
-        // Use the most recently released API file bounded by toApi.
-        getLastReleasedApiFile(rootFolder, toApi, false, false)
-    }
-}
-
 private fun getLastReleasedApiFile(
     rootFolder: File,
     refVersion: Version?,
@@ -429,46 +159,3 @@
     }
     return lastFile
 }
-
-// Creates a new task on the project for verifying the API
-private fun createCheckApiTask(
-    project: Project,
-    taskName: String,
-    docletpath: Collection<File>,
-    config: ChecksConfig,
-    oldApi: File?,
-    newApi: Provider<File>,
-    whitelist: File? = null
-) =
-        project.tasks.register(taskName, CheckApiTask::class.java) {
-            it.apply {
-                doclavaClasspath = docletpath
-                checksConfig = config
-                newApiFile = newApi.get()
-                oldApiFile = oldApi
-                whitelistErrorsFile = whitelist
-                doFirst {
-                    logger.lifecycle("Verifying ${newApi.get().name} " +
-                            "against ${oldApi?.name ?: "nothing"}...")
-                }
-            }
-        }
-
-private fun getNewApiTxt(project: Project, generateApi: TaskProvider<DoclavaTask>): FileProvider {
-    val toApi = project.processProperty("toApi")
-    return if (toApi != null) {
-        // Use an explicit API file.
-        FileProvider(project.provider {
-            File(project.projectDir, "api/$toApi.txt")
-        }, null)
-    } else {
-        // Use the current API file (e.g. current.txt).
-        FileProvider(generateApi.map {
-            it.apiFile!!
-        }, generateApi)
-    }
-}
-
-private data class FileProvider(val file: Provider<File>, val task: TaskProvider<*>?)
-
-private fun stripExtension(fileName: String) = fileName.substringBeforeLast('.')
diff --git a/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApiTask.kt b/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApiTask.kt
deleted file mode 100644
index a183735..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApiTask.kt
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.checkapi
-
-import androidx.build.doclava.ChecksConfig
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleException
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.security.MessageDigest
-
-/** Character that resets console output color. */
-private const val ANSI_RESET = "\u001B[0m"
-
-/** Character that sets console output color to red. */
-private const val ANSI_RED = "\u001B[31m"
-
-/** Character that sets console output color to yellow. */
-private const val ANSI_YELLOW = "\u001B[33m"
-
-private val ERROR_REGEX = Regex("^(.+):(.+): (\\w+) (\\d+): (.+)$")
-
-private fun ByteArray.encodeHex() = fold(StringBuilder(), { builder, byte ->
-    val hexString = Integer.toHexString(byte.toInt() and 0xFF)
-    if (hexString.length < 2) {
-        builder.append("0")
-    }
-    builder.append(hexString)
-}).toString()
-
-private fun getShortHash(src: String): String {
-    val str = MessageDigest.getInstance("SHA-1")
-            .digest(src.toByteArray()).encodeHex()
-    val len = str.length
-    return str.substring(len - 7, len)
-}
-
-/**
- * Task used to verify changes between two API files.
- * <p>
- * This task may be configured to ignore, warn, or fail with a message for a specific set of
- * Doclava-defined error codes. See {@link com.google.doclava.Errors} for a complete list of
- * supported error codes.
- * <p>
- * Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each
- * SHA is unique to a specific API change and is logged to the error output on failure.
- */
-open class CheckApiTask : DefaultTask() {
-
-    /** API file that represents the existing API surface. */
-    @Optional
-    @InputFile
-    var oldApiFile: File? = null
-        get() {
-            val result = field
-            if (result != null && !result.exists()) {
-                throw GradleException("Can't check api against non-existent file $result. " +
-                        "Please run updateApi to fix that.")
-            }
-            return result
-        }
-
-    /** API file that represents the existing API surface's removals. */
-    @Optional
-    @InputFile
-    var oldRemovedApiFile: File? = null
-
-    /** API file that represents the candidate API surface. */
-    lateinit var newApiFile: File
-
-//  defines input. (newApiFile may not exist, so we want to create in this case)
-    @Suppress("unused")
-    @InputFile
-    fun getNewApiFileForInputs(): File {
-        if (!newApiFile.exists()) {
-            newApiFile.parentFile.mkdirs()
-            newApiFile.createNewFile()
-        }
-        return newApiFile
-    }
-
-    /** API file that represents the candidate API surface's removals. */
-    @Optional
-    @InputFile
-    var newRemovedApiFile: File? = null
-
-    /** Optional file containing a newline-delimited list of error SHAs to ignore. */
-    var whitelistErrorsFile: File? = null
-
-    @Optional
-    @InputFile
-    fun getWhiteListErrorsFileInput(): File? {
-        // Gradle requires non-null InputFiles to exist -- even with Optional -- so work around that
-        // by returning null for this field if the file doesn't exist.
-        if (whitelistErrorsFile?.exists() == true) {
-            return whitelistErrorsFile
-        }
-        return null
-    }
-
-    /**
-     * Optional set of error SHAs to ignore.
-     * <p>
-     * Each error SHA is unique to a specific API change.
-     */
-    @Optional
-    @Input
-    var whitelistErrors = emptySet<String>()
-
-    var detectedWhitelistErrors = mutableSetOf<String>()
-
-    @InputFiles
-    var doclavaClasspath: Collection<File> = emptyList()
-
-    // A dummy output file meant only to tag when this check was last ran.
-    // Without any outputs, Gradle will run this task every time.
-    @Optional
-    private var mOutputFile: File? = null
-
-    @OutputFile
-    fun getOutputFile(): File {
-        return if (mOutputFile != null) {
-            mOutputFile!!
-        } else {
-            File(project.buildDir, "checkApi/$name-completed")
-        }
-    }
-
-    @Optional
-    fun setOutputFile(outputFile: File) {
-        mOutputFile = outputFile
-    }
-
-    @Input
-    lateinit var checksConfig: ChecksConfig
-
-    init {
-        group = "Verification"
-        description = "Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date."
-    }
-
-    private fun collectAndVerifyInputs(): Set<File> {
-        if (oldRemovedApiFile != null && newRemovedApiFile != null) {
-            return setOf(oldApiFile!!, newApiFile, oldRemovedApiFile!!, newRemovedApiFile!!)
-        } else {
-            return setOf(oldApiFile!!, newApiFile)
-        }
-    }
-
-    @TaskAction
-    fun exec() {
-        if (oldApiFile == null) {
-            // Nothing to do.
-            return
-        }
-
-        val apiFiles = collectAndVerifyInputs()
-
-        val errStream = ByteArrayOutputStream()
-
-        // If either of those gets tweaked, then this should be refactored to extend JavaExec.
-        project.javaexec { spec ->
-            spec.apply {
-                // Put Doclava on the classpath so we can get the ApiCheck class.
-                classpath(doclavaClasspath)
-                main = "com.google.doclava.apicheck.ApiCheck"
-
-                minHeapSize = "128m"
-                maxHeapSize = "1024m"
-
-                // add -error LEVEL for every error level we want to fail the build on.
-                checksConfig.errors.forEach { args("-error", it) }
-                checksConfig.warnings.forEach { args("-warning", it) }
-                checksConfig.hidden.forEach { args("-hide", it) }
-
-                spec.args(apiFiles.map { it.absolutePath })
-
-                // Redirect error output so that we can whitelist specific errors.
-                errorOutput = errStream
-                // We will be handling failures ourselves with a custom message.
-                setIgnoreExitValue(true)
-            }
-        }
-
-        // Load the whitelist file, if present.
-        val whitelistFile = whitelistErrorsFile
-        if (whitelistFile?.exists() == true) {
-            whitelistErrors += whitelistFile.readLines()
-        }
-
-        // Parse the error output.
-        val unparsedErrors = mutableSetOf<String>()
-        val detectedErrors = mutableSetOf<List<String>>()
-        val parsedErrors = mutableSetOf<List<String>>()
-        ByteArrayInputStream(errStream.toByteArray()).bufferedReader().lines().forEach {
-            val match = ERROR_REGEX.matchEntire(it)
-
-            if (match == null) {
-                unparsedErrors.add(it)
-            } else if (match.groups[3]?.value == "error") {
-                val hash = getShortHash(match.groups[5]?.value!!)
-                val error = match.groupValues.subList(1, match.groupValues.size) + listOf(hash)
-                if (hash in whitelistErrors) {
-                    detectedErrors.add(error)
-                    detectedWhitelistErrors.add(error[5])
-                } else {
-                    parsedErrors.add(error)
-                }
-            }
-        }
-
-        unparsedErrors.forEach { error -> logger.error("$ANSI_RED$error$ANSI_RESET") }
-        parsedErrors.forEach { logger.error("$ANSI_RED${it[5]}$ANSI_RESET ${it[4]}") }
-        detectedErrors.forEach { logger.warn("$ANSI_YELLOW${it[5]}$ANSI_RESET ${it[4]}") }
-
-        if (unparsedErrors.isNotEmpty() || parsedErrors.isNotEmpty()) {
-            throw GradleException(checksConfig.onFailMessage ?: "")
-        }
-
-        // Just create a dummy file upon completion. Without any outputs, Gradle will run this task
-        // every time.
-        val outputFile = getOutputFile()
-        outputFile.parentFile.mkdirs()
-        outputFile.createNewFile()
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/androidx/build/checkapi/UpdateApiTask.kt b/buildSrc/src/main/kotlin/androidx/build/checkapi/UpdateApiTask.kt
deleted file mode 100644
index 26a0e6a..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/checkapi/UpdateApiTask.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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.build.checkapi
-
-import androidx.build.Version
-import com.google.common.io.Files
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleException
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-import java.nio.charset.Charset
-import java.util.HashSet
-
-/**
- * Task for updating the checked in API file with the newly generated one.
- */
-open class UpdateApiTask : DefaultTask() {
-    @get:InputFile
-    lateinit var newApiFile: File
-    @get:[InputFile Optional]
-    var newRemovedApiFile: File? = null
-
-    @get:[Input Optional]
-    var whitelistErrors: Set<String> = HashSet()
-
-    @get:OutputFile
-    lateinit var oldApiFile: File
-    @get:[OutputFile Optional]
-    var oldRemovedApiFile: File? = null
-
-    @get:[OutputFile Optional]
-    var whitelistErrorsFile: File? = null
-
-    /**
-     * Actually copy the file to the desired location and update the whitelist warnings file.
-     */
-    @TaskAction
-    fun doUpdate() {
-        if (oldApiFile.exists() && newApiFile.readText() == oldApiFile.readText()) {
-            // whatever nothing changed
-            return
-        }
-
-        val version = Version(project.version as String)
-        if (version.isPatch()) {
-            throw GradleException("Public APIs may not be modified in patch releases.")
-        } else if (version.isFinalApi() && oldApiFile.exists() && !project.hasProperty("force")) {
-            throw GradleException("Public APIs may not be modified in finalized releases.")
-        }
-
-        Files.copy(newApiFile, oldApiFile)
-
-        if (oldApiFile.name != "current.txt") {
-            Files.copy(newApiFile, File(project.projectDir, "api/current.txt"))
-        }
-
-        if (oldRemovedApiFile != null) {
-            if (newRemovedApiFile != null) {
-                Files.copy(newRemovedApiFile!!, oldRemovedApiFile!!)
-            } else {
-                oldRemovedApiFile!!.delete()
-            }
-        }
-
-        if (whitelistErrorsFile != null && !whitelistErrors.isEmpty()) {
-            Files.newWriter(
-                    whitelistErrorsFile!!, Charset.defaultCharset()).use { writer ->
-                for (error in whitelistErrors) {
-                    writer.write(error + "\n")
-                }
-            }
-            logger.lifecycle("Whitelisted " + whitelistErrors.size + " error(s)...")
-        }
-
-        logger.lifecycle("Wrote public API definition to " + oldApiFile.name)
-    }
-}
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index e9021ae..a3404b7 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -46,10 +46,10 @@
 const val JUNIT = "junit:junit:4.12"
 const val KOTLINPOET = "com.squareup:kotlinpoet:1.1.0"
 
-private const val KOTLIN_VERSION = "1.3.20"
+private const val KOTLIN_VERSION = "1.3.31"
 const val KOTLIN_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION"
 const val KOTLIN_TEST_COMMON = "org.jetbrains.kotlin:kotlin-test:$KOTLIN_VERSION"
-const val COMPOSE_VERSION="1.3.30-compose-20190503"
+const val COMPOSE_VERSION = "1.3.30-compose-20190503"
 const val KOTLIN_COMPOSE_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:$COMPOSE_VERSION"
 const val KOTLIN_COMPOSE_REFLECT = "org.jetbrains.kotlin:kotlin-reflect:$COMPOSE_VERSION"
 
diff --git a/buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt b/buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt
deleted file mode 100644
index 54e9ec6..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/jdiff/JDiffTask.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.build.jdiff
-
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.javadoc.Javadoc
-import org.gradle.external.javadoc.CoreJavadocOptions
-import org.gradle.external.javadoc.MinimalJavadocOptions
-
-import java.io.File
-import java.util.ArrayList
-
-/**
- * JDiff task to compare API changes.
- */
-open class JDiffTask : Javadoc() {
-
-    /**
-     * Sets the doclet path, which will be used to locate the `com.google.doclava.Doclava` class.
-     *
-     *
-     * This option will override any doclet path set in this instance's
-     * [JavadocOptions][.getOptions].
-     *
-     * @see MinimalJavadocOptions.setDocletpath
-     */
-    @get:InputFiles
-    var docletpath: Collection<File>? = null
-        set(docletpath) {
-            field = docletpath
-            // Go ahead and keep the mDocletpath in our JavadocOptions object in sync.
-            options.docletpath = ArrayList(docletpath)
-        }
-
-    @get:InputFile
-    lateinit var oldApiXmlFile: File
-
-    @get:InputFile
-    lateinit var newApiXmlFile: File
-
-    /**
-     * Relative path to the Javadoc corresponding to the old API, relative to
-     * "${destinationDir}/changes". Should end with the directory separator (usually '/').
-     */
-    @get:[Input Optional]
-    var oldJavadocPrefix: String? = null
-
-    /**
-     * Relative path to the Javadoc corresponding to the new API, relative to
-     * "${destinationDir}/changes". Should end with the directory separator (usually '/').
-     */
-    @get:[Input Optional]
-    var newJavadocPrefix: String? = null
-
-    // HTML diff files will be placed in destinationDir, which is defined by the superclass.
-
-    @get:Input
-    var stats = true
-
-    init {
-        isFailOnError = true
-        options.doclet = "jdiff.JDiff"
-        options.encoding = "UTF-8"
-        maxMemory = "1280m"
-    }
-
-    /**
-     * Configures this JDiffTask with parameters that might not be at their final values
-     * until this task is run.
-     */
-    private fun configureJDiffTask() {
-        val options = options as CoreJavadocOptions
-
-        options.docletpath = ArrayList(docletpath!!)
-
-        if (stats) {
-            options.addStringOption("stats")
-        }
-
-        val oldApiXmlFileDir = oldApiXmlFile.parentFile
-        val newApiXmlFileDir = newApiXmlFile.parentFile
-
-        if (oldApiXmlFileDir.exists()) {
-            options.addStringOption("oldapidir", oldApiXmlFileDir.absolutePath)
-        }
-        // For whatever reason, jdiff appends .xml to the file name on its own.
-        // Strip the .xml off the end of the file name
-        options.addStringOption("oldapi",
-                oldApiXmlFile.name.substring(0, oldApiXmlFile.name.length - 4))
-        if (newApiXmlFileDir.exists()) {
-            options.addStringOption("newapidir", newApiXmlFileDir.absolutePath)
-        }
-        options.addStringOption("newapi",
-                newApiXmlFile.name.substring(0, newApiXmlFile.name.length - 4))
-
-        if (oldJavadocPrefix != null) {
-            options.addStringOption("javadocold", oldJavadocPrefix)
-        }
-        if (newJavadocPrefix != null) {
-            options.addStringOption("javadocnew", newJavadocPrefix)
-        }
-    }
-
-    public override fun generate() {
-        configureJDiffTask()
-        super.generate()
-    }
-}
diff --git a/buildSrc/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt b/buildSrc/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt
index b6c9d97..ca23588 100644
--- a/buildSrc/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt
+++ b/buildSrc/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt
@@ -37,7 +37,6 @@
                 """
                     platform/external/doclava 798edee4d01239248ed33585a42b19ca93c31f56
                     platform/external/flatbuffers b5c9b8e12b47d0597cd29b217e5765d325957d8a
-                    platform/external/jdiff fe19ba361e4a1230108378b4634784281dd9d250
                     platform/external/noto-fonts d20a289eaebf2e371064dacc306b57d37b733f7c
                     platform/frameworks/support fc9030edfe7e0a85a5545a342c7efbf93283f62f
                     platform/manifest 446c4307abb8d676b308af55b06e1d1a01c858c7
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Config.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
index 39c4ce8..f0e4917 100644
--- a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
@@ -27,6 +27,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.camera2.impl.CameraEventCallbacks;
 import androidx.camera.core.Config;
 import androidx.camera.core.MutableConfig;
 import androidx.camera.core.MutableOptionsBundle;
@@ -55,6 +56,11 @@
                     CameraCaptureSession.StateCallback.class);
     static final Option<CaptureCallback> SESSION_CAPTURE_CALLBACK_OPTION =
             Option.create("camera2.cameraCaptureSession.captureCallback", CaptureCallback.class);
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
+    public static final Option<CameraEventCallbacks> CAMERA_EVENT_CALLBACK_OPTION =
+            Option.create("camera2.cameraEvent.callback", CameraEventCallbacks.class);
     // *********************************************************************************************
 
     private final Config mConfig;
@@ -163,6 +169,17 @@
         return mConfig.retrieveOption(SESSION_CAPTURE_CALLBACK_OPTION, valueIfMissing);
     }
 
+    /**
+     * Returns the stored CameraEventCallbacks instance.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public CameraEventCallbacks getCameraEventCallback(CameraEventCallbacks valueIfMissing) {
+        return mConfig.retrieveOption(CAMERA_EVENT_CALLBACK_OPTION, valueIfMissing);
+    }
+
     // Start of the default implementation of Config
     // *********************************************************************************************
 
@@ -321,6 +338,18 @@
                     captureCallback);
             return this;
         }
+
+        /**
+         * Sets a CameraEventCallbacks instance.
+         *
+         * @param cameraEventCallbacks The CameraEventCallbacks.
+         * @return The current Extender.
+         */
+        public Extender setCameraEventCallback(CameraEventCallbacks cameraEventCallbacks) {
+            mBaseBuilder.getMutableConfig().insertOption(CAMERA_EVENT_CALLBACK_OPTION,
+                    cameraEventCallbacks);
+            return this;
+        }
     }
 
     /**
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera.java
index 9498a3e..0ecf8f8 100644
--- a/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera.java
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera.java
@@ -254,6 +254,7 @@
 
     void closeCameraResource() {
         mCaptureSession.close();
+        mCaptureSession.release();
         mCameraDevice.close();
         notifyCameraDeviceCloseToCaptureSessions();
         mCameraDevice = null;
@@ -623,6 +624,7 @@
         SessionConfig previousSessionConfig = mCaptureSession.getSessionConfig();
 
         mCaptureSession.close();
+        mCaptureSession.release();
 
         // Saves the closed CaptureSessions if device is not closed yet.
         // We need to notify camera device closed event to these CaptureSessions.
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera2OptionUnpacker.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera2OptionUnpacker.java
index f76f343..45e0351 100644
--- a/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera2OptionUnpacker.java
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/Camera2OptionUnpacker.java
@@ -23,6 +23,7 @@
 import androidx.camera.core.CameraDeviceStateCallbacks;
 import androidx.camera.core.Config;
 import androidx.camera.core.Config.Option;
+import androidx.camera.core.MutableOptionsBundle;
 import androidx.camera.core.OptionsBundle;
 import androidx.camera.core.SessionConfig;
 import androidx.camera.core.UseCaseConfig;
@@ -77,6 +78,11 @@
                         camera2Config.getSessionCaptureCallback(
                                 Camera2CaptureCallbacks.createNoOpCallback())));
 
+        MutableOptionsBundle cameraEventConfig = MutableOptionsBundle.create();
+        cameraEventConfig.insertOption(Camera2Config.CAMERA_EVENT_CALLBACK_OPTION,
+                camera2Config.getCameraEventCallback(CameraEventCallbacks.createEmptyCallback()));
+        builder.addImplementationOptions(cameraEventConfig);
+
         // Copy extension keys
         camera2Config.findOptions(
                 Camera2Config.CAPTURE_REQUEST_ID_STEM,
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallback.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallback.java
new file mode 100644
index 0000000..e364e9d
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallback.java
@@ -0,0 +1,79 @@
+/*
+ * 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.camera.camera2.impl;
+
+import android.hardware.camera2.CameraCaptureSession;
+
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CaptureConfig;
+
+/**
+ * A callback object for tracking the camera capture session event and get request data.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class CameraEventCallback {
+
+    /**
+     * This will be invoked before creating a {@link CameraCaptureSession}. The returned
+     * parameter in CaptureConfig will be passed to the camera device as part of the capture session
+     * initialization via setSessionParameters(). The valid parameter is a subset of the
+     * available capture request parameters.
+     *
+     * @return CaptureConfig The request information to customize the session.
+     */
+    public CaptureConfig onPresetSession() {
+        return null;
+    }
+
+    /**
+     * This will be invoked once after a {@link CameraCaptureSession} is created. The returned
+     * parameter in CaptureConfig will be used to generate a single request to the current
+     * configured camera device. The generated request would be submitted to camera before process
+     * other single request.
+     *
+     * @return CaptureConfig The request information to customize the session.
+     */
+    public CaptureConfig onEnableSession() {
+        return null;
+    }
+
+    /**
+     * This callback will be invoked before starting the repeating request in the
+     * {@link CameraCaptureSession}. The returned CaptureConfig will be used to generate a
+     * capture request, and would be used in setRepeatingRequest().
+     *
+     * @return CaptureConfig The request information to customize the session.
+     */
+    public CaptureConfig onRepeating() {
+        return null;
+    }
+
+    /**
+     * This will be invoked once before the {@link CameraCaptureSession} is closed. The
+     * returned parameter in CaptureConfig will be used to generate a single request to the current
+     * configured camera device. The generated request would be submitted to camera before the
+     * capture session was closed.
+     *
+     * @return CaptureConfig The request information to customize the session.
+     */
+    public CaptureConfig onDisableSession() {
+        return null;
+    }
+
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallbacks.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallbacks.java
new file mode 100644
index 0000000..a5187c8d
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallbacks.java
@@ -0,0 +1,145 @@
+/*
+ * 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.camera.camera2.impl;
+
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CaptureConfig;
+import androidx.camera.core.MultiValueSet;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraEventCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class CameraEventCallbacks extends MultiValueSet<CameraEventCallback> {
+
+    public CameraEventCallbacks(CameraEventCallback ... callbacks) {
+        addAll(Arrays.asList(callbacks));
+    }
+
+    /** Returns a camera event callback which calls a list of other callbacks. */
+    public ComboCameraEventCallback createComboCallback() {
+        return new ComboCameraEventCallback(getAllItems());
+    }
+
+    /** Returns a camera event callback which does nothing. */
+    public static CameraEventCallbacks createEmptyCallback() {
+        return new CameraEventCallbacks();
+    }
+
+    @Override
+    public MultiValueSet<CameraEventCallback> clone() {
+        CameraEventCallbacks ret = createEmptyCallback();
+        ret.addAll(getAllItems());
+        return ret;
+    }
+
+    /**
+     * A CameraEventCallback which contains a list of CameraEventCallback and will
+     * propagate received callback to the list.
+     */
+    public static final class ComboCameraEventCallback {
+        private final List<CameraEventCallback> mCallbacks = new ArrayList<>();
+
+        ComboCameraEventCallback(List<CameraEventCallback> callbacks) {
+            for (CameraEventCallback callback : callbacks) {
+                mCallbacks.add(callback);
+            }
+        }
+
+        /**
+         * To Invoke {@link CameraEventCallback#onPresetSession()} on the set of list and
+         * aggregated the results to a set list.
+         *
+         * @return List<CaptureConfig> The request information to customize the session.
+         */
+        public List<CaptureConfig> onPresetSession() {
+            List<CaptureConfig> ret = new LinkedList<>();
+            for (CameraEventCallback callback : mCallbacks) {
+                CaptureConfig presetCaptureStage = callback.onPresetSession();
+                if (presetCaptureStage != null) {
+                    ret.add(presetCaptureStage);
+                }
+            }
+            return ret;
+        }
+
+        /**
+         * To Invoke {@link CameraEventCallback#onEnableSession()} on the set of list and
+         * aggregated the results to a set list.
+         *
+         * @return List<CaptureConfig> The request information to customize the session.
+         */
+        public List<CaptureConfig> onEnableSession() {
+            List<CaptureConfig> ret = new LinkedList<>();
+            for (CameraEventCallback callback : mCallbacks) {
+                CaptureConfig enableCaptureStage = callback.onEnableSession();
+                if (enableCaptureStage != null) {
+                    ret.add(enableCaptureStage);
+                }
+            }
+            return ret;
+        }
+
+        /**
+         * To Invoke {@link CameraEventCallback#onRepeating()} on the set of list and
+         * aggregated the results to a set list.
+         *
+         * @return List<CaptureConfig> The request information to customize the session.
+         */
+        public List<CaptureConfig> onRepeating() {
+            List<CaptureConfig> ret = new LinkedList<>();
+            for (CameraEventCallback callback : mCallbacks) {
+                CaptureConfig repeatingCaptureStage = callback.onRepeating();
+                if (repeatingCaptureStage != null) {
+                    ret.add(repeatingCaptureStage);
+                }
+            }
+            return ret;
+        }
+
+        /**
+         * To Invoke {@link CameraEventCallback#onDisableSession()} on the set of list and
+         * aggregated the results to a set list.
+         *
+         * @return List<CaptureConfig> The request information to customize the session.
+         */
+        public List<CaptureConfig> onDisableSession() {
+            List<CaptureConfig> ret = new LinkedList<>();
+            for (CameraEventCallback callback : mCallbacks) {
+                CaptureConfig disableCaptureStage = callback.onDisableSession();
+                if (disableCaptureStage != null) {
+                    ret.add(disableCaptureStage);
+                }
+            }
+            return ret;
+        }
+
+        @NonNull
+        public List<CameraEventCallback> getCallbacks() {
+            return mCallbacks;
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/CaptureSession.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/CaptureSession.java
index 388efbc..d8fe2bc 100644
--- a/camera/camera2/src/main/java/androidx/camera/camera2/impl/CaptureSession.java
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/CaptureSession.java
@@ -22,7 +22,11 @@
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.os.Build;
 import android.os.Handler;
+import android.os.Looper;
 import android.util.Log;
 import android.view.Surface;
 
@@ -36,11 +40,16 @@
 import androidx.camera.core.Config.Option;
 import androidx.camera.core.DeferrableSurface;
 import androidx.camera.core.DeferrableSurfaces;
+import androidx.camera.core.ImmediateSurface;
 import androidx.camera.core.SessionConfig;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
 
 /**
  * A session for capturing images from the camera which is tied to a specific {@link CameraDevice}.
@@ -54,6 +63,8 @@
     /** Handler for all the callbacks from the {@link CameraCaptureSession}. */
     @Nullable
     private final Handler mHandler;
+    /** An adapter to pass the task to the handler. */
+    private final Executor mExecutor;
     /** The configuration for the currently issued single capture requests. */
     private final List<CaptureConfig> mCaptureConfigs = new ArrayList<>();
     /** Lock on whether the camera is open or closed. */
@@ -80,6 +91,8 @@
     /** The list of DeferrableSurface used to notify surface detach events */
     @GuardedBy("mConfiguredDeferrableSurfaces")
     List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList();
+    /** The list of DeferrableSurface used for repeating request */
+    private List<DeferrableSurface> mConfiguredRepeatingSurfaces = Collections.emptyList();
     /** Tracks the current state of the session. */
     @GuardedBy("mStateLock")
     State mState = State.UNINITIALIZED;
@@ -95,6 +108,7 @@
     CaptureSession(@Nullable Handler handler) {
         this.mHandler = handler;
         mState = State.INITIALIZED;
+        mExecutor = new ExecutorHandlerAdapter(handler);
     }
 
     /**
@@ -185,6 +199,15 @@
                         return;
                     }
 
+                    // TODO(b/132664086): Remove this workaround after aosp/955625 has been
+                    //  submitted.
+                    Set<Surface> repeatingSurfaces = DeferrableSurfaces.surfaceSet(
+                            sessionConfig.getRepeatingCaptureConfig().getSurfaces());
+                    mConfiguredRepeatingSurfaces = new ArrayList<>();
+                    for (Surface s : repeatingSurfaces) {
+                        mConfiguredRepeatingSurfaces.add(new ImmediateSurface(s));
+                    }
+
                     notifySurfaceAttached();
                     mState = State.OPENING;
                     Log.d(TAG, "Opening capture session.");
@@ -193,7 +216,42 @@
                     callbacks.add(mCaptureSessionStateCallback);
                     CameraCaptureSession.StateCallback comboCallback =
                             CameraCaptureSessionStateCallbacks.createComboCallback(callbacks);
-                    cameraDevice.createCaptureSession(mConfiguredSurfaces, comboCallback, mHandler);
+
+                    // Start check preset CaptureStage information.
+                    CameraEventCallbacks eventCallbacks = new Camera2Config(
+                            sessionConfig.getImplementationOptions()).getCameraEventCallback(
+                            CameraEventCallbacks.createEmptyCallback());
+                    List<CaptureConfig> presetList =
+                            eventCallbacks.createComboCallback().onPresetSession();
+
+                    // Generate the CaptureRequest builder from repeating request since Android
+                    // recommend use the same template type as the initial capture request. The
+                    // tag and output targets would be ignored by default.
+                    CaptureRequest.Builder builder =
+                            sessionConfig.getRepeatingCaptureConfig().buildCaptureRequestNoTarget(
+                                    cameraDevice);
+
+                    if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P
+                            && builder != null && !presetList.isEmpty()) {
+                        for (CaptureConfig config : presetList) {
+                            applyImplementationOptionTCaptureBuilder(builder,
+                                    config.getImplementationOptions());
+                        }
+
+                        List<OutputConfiguration> outputConfigList = new LinkedList<>();
+                        for (Surface surface : mConfiguredSurfaces) {
+                            outputConfigList.add(new OutputConfiguration(surface));
+                        }
+
+                        SessionConfiguration sessionParameterConfiguration =
+                                new SessionConfiguration(SessionConfiguration.SESSION_REGULAR,
+                                        outputConfigList, mExecutor, comboCallback);
+                        sessionParameterConfiguration.setSessionParameters(builder.build());
+                        cameraDevice.createCaptureSession(sessionParameterConfiguration);
+                    } else {
+                        cameraDevice.createCaptureSession(mConfiguredSurfaces, comboCallback,
+                                mHandler);
+                    }
                     break;
                 default:
                     Log.e(TAG, "Open not allowed in state: " + mState);
@@ -219,8 +277,20 @@
                 case INITIALIZED:
                     mState = State.RELEASED;
                     break;
-                case OPENING:
                 case OPENED:
+                    // Only issue onDisableSession requests at OPENED state.
+                    if (mSessionConfig != null) {
+                        CameraEventCallbacks eventCallbacks = new Camera2Config(
+                                mSessionConfig.getImplementationOptions()).getCameraEventCallback(
+                                CameraEventCallbacks.createEmptyCallback());
+                        List<CaptureConfig> configList =
+                                eventCallbacks.createComboCallback().onDisableSession();
+                        if (!configList.isEmpty()) {
+                            issueCaptureRequests(setupConfiguredSurface(configList));
+                        }
+                    }
+                    // Not break close flow.
+                case OPENING:
                     mState = State.CLOSED;
                     mSessionConfig = null;
                     break;
@@ -255,7 +325,9 @@
                     break;
                 case OPENED:
                 case CLOSED:
-                    mCameraCaptureSession.close();
+                    if (mCameraCaptureSession != null) {
+                        mCameraCaptureSession.close();
+                    }
                     mState = State.RELEASING;
                     break;
                 case RELEASING:
@@ -350,6 +422,16 @@
                 return;
             }
 
+            CameraEventCallbacks eventCallbacks = new Camera2Config(
+                    mSessionConfig.getImplementationOptions()).getCameraEventCallback(
+                    CameraEventCallbacks.createEmptyCallback());
+            List<CaptureConfig> repeatingRequestList =
+                    eventCallbacks.createComboCallback().onRepeating();
+            for (CaptureConfig config : repeatingRequestList) {
+                applyImplementationOptionTCaptureBuilder(builder,
+                        config.getImplementationOptions());
+            }
+
             applyImplementationOptionTCaptureBuilder(
                     builder, captureConfig.getImplementationOptions());
 
@@ -504,6 +586,20 @@
                     case OPENING:
                         mState = State.OPENED;
                         mCameraCaptureSession = session;
+
+                        // Issue capture request of enableSession if exists.
+                        if (mSessionConfig != null) {
+                            Config implOptions = mSessionConfig.getImplementationOptions();
+                            CameraEventCallbacks eventCallbacks = new Camera2Config(
+                                    implOptions).getCameraEventCallback(
+                                    CameraEventCallbacks.createEmptyCallback());
+                            List<CaptureConfig> list =
+                                    eventCallbacks.createComboCallback().onEnableSession();
+                            if (!list.isEmpty()) {
+                                issueCaptureRequests(setupConfiguredSurface(list));
+                            }
+                        }
+
                         Log.d(TAG, "Attempting to send capture request onConfigured");
                         issueRepeatingCaptureRequests();
                         issueBurstCaptureRequest();
@@ -578,4 +674,47 @@
     public void notifyCameraDeviceClose() {
         notifySurfaceDetached();
     }
+
+    List<CaptureConfig> setupConfiguredSurface(List<CaptureConfig> list) {
+        List<CaptureConfig> ret = new ArrayList<>();
+        for (CaptureConfig c : list) {
+            CaptureConfig.Builder builder = CaptureConfig.Builder.from(c);
+            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            for (DeferrableSurface s : mConfiguredRepeatingSurfaces) {
+                builder.addSurface(s);
+            }
+            ret.add(builder.build());
+        }
+
+        return ret;
+    }
+
+    private static class ExecutorHandlerAdapter implements Executor {
+
+        private final Handler mHandler;
+        ExecutorHandlerAdapter(Handler handler) {
+            mHandler = handler;
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            Handler handler;
+
+            if (mHandler == null) {
+                // Run in current thread when handler not exist.
+                Looper looper = Looper.myLooper();
+                if (looper == null) {
+                    throw new IllegalArgumentException(
+                            "No handler given, and current thread has no looper!");
+                }
+                handler = new Handler(looper);
+            } else {
+                handler = mHandler;
+            }
+
+            if (!handler.post(command)) {
+                throw new RejectedExecutionException(handler + " is shutting down");
+            }
+        }
+    }
 }
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
index d1cf114..2798b0f 100644
--- a/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
@@ -163,6 +163,23 @@
         return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
     }
 
+    /** @hide */
+    @Nullable
+    @Override
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public UseCase.EventListener getUseCaseEventListener(
+            @Nullable UseCase.EventListener valueIfMissing) {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER, valueIfMissing);
+    }
+
+    /** @hide */
+    @Nullable
+    @Override
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public UseCase.EventListener getUseCaseEventListener() {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER);
+    }
+
     // End of the default implementation of Config
     // *********************************************************************************************
 
@@ -245,5 +262,13 @@
             getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
             return this;
         }
+
+        /** @hide */
+        @Override
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder setUseCaseEventListener(UseCase.EventListener eventListener) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_LISTENER, eventListener);
+            return this;
+        }
     }
 }
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureConfig.java b/camera/core/src/main/java/androidx/camera/core/CaptureConfig.java
index 6e89a7b..9bc8547 100644
--- a/camera/core/src/main/java/androidx/camera/core/CaptureConfig.java
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureConfig.java
@@ -166,6 +166,31 @@
     }
 
     /**
+     * TODO(b/132664086): To replace this old implementation by Camera2CaptureRequestBuilder once
+     *  aosp/955625 was submitted.
+     *
+     * Return the builder of a {@link CaptureRequest} which include capture request parameters and
+     * desired template type, but no target surfaces and tag.
+     *
+     * <p>Returns {@code null} if a valid {@link CaptureRequest} can not be constructed.
+     */
+    @Nullable
+    public CaptureRequest.Builder buildCaptureRequestNoTarget(@Nullable CameraDevice device)
+            throws CameraAccessException {
+        if (device == null) {
+            return null;
+        }
+        CaptureRequest.Builder builder = device.createCaptureRequest(mTemplateType);
+
+        for (CaptureRequestParameter<?> captureRequestParameter :
+                mCaptureRequestParameters.values()) {
+            captureRequestParameter.apply(builder);
+        }
+
+        return builder;
+    }
+
+    /**
      * Builder for easy modification/rebuilding of a {@link CaptureConfig}.
      *
      * @hide
@@ -274,7 +299,17 @@
             for (Option<?> option : config.listOptions()) {
                 @SuppressWarnings("unchecked") // Options/values are being copied directly
                         Option<Object> objectOpt = (Option<Object>) option;
-                mImplementationOptions.insertOption(objectOpt, config.retrieveOption(objectOpt));
+
+                Object existValue = mImplementationOptions.retrieveOption(objectOpt, null);
+                Object newValue = config.retrieveOption(objectOpt);
+                if (existValue instanceof MultiValueSet) {
+                    ((MultiValueSet) existValue).addAll(((MultiValueSet) newValue).getAllItems());
+                } else {
+                    if (newValue instanceof MultiValueSet) {
+                        newValue = ((MultiValueSet) newValue).clone();
+                    }
+                    mImplementationOptions.insertOption(objectOpt, newValue);
+                }
             }
         }
 
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageAnalysisConfig.java b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisConfig.java
index 849b9a7..63324b9 100644
--- a/camera/core/src/main/java/androidx/camera/core/ImageAnalysisConfig.java
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -411,15 +411,32 @@
         return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
     }
 
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener(
+            @Nullable UseCase.EventListener valueIfMissing) {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener() {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER);
+    }
+
     // End of the default implementation of Config
     // *********************************************************************************************
 
     /** Builder for a {@link ImageAnalysisConfig}. */
     public static final class Builder
-            implements CameraDeviceConfig.Builder<ImageAnalysisConfig.Builder>,
-            ImageOutputConfig.Builder<ImageAnalysisConfig.Builder>,
-            ThreadConfig.Builder<ImageAnalysisConfig.Builder>,
-            UseCaseConfig.Builder<ImageAnalysis, ImageAnalysisConfig, ImageAnalysisConfig.Builder> {
+            implements CameraDeviceConfig.Builder<Builder>,
+            ImageOutputConfig.Builder<Builder>,
+            ThreadConfig.Builder<Builder>,
+            UseCaseConfig.Builder<ImageAnalysis, ImageAnalysisConfig, Builder> {
 
         private final MutableOptionsBundle mMutableConfig;
 
@@ -666,5 +683,13 @@
             getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setUseCaseEventListener(UseCase.EventListener useCaseEventListener) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_LISTENER, useCaseEventListener);
+            return this;
+        }
     }
 }
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageCaptureConfig.java b/camera/core/src/main/java/androidx/camera/core/ImageCaptureConfig.java
index 8453aa3..237b27d8 100644
--- a/camera/core/src/main/java/androidx/camera/core/ImageCaptureConfig.java
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCaptureConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -40,9 +40,9 @@
     // Option Declarations:
     // *********************************************************************************************
 
-    static final Option<ImageCapture.CaptureMode> OPTION_IMAGE_CAPTURE_MODE =
+    static final Option<CaptureMode> OPTION_IMAGE_CAPTURE_MODE =
             Option.create(
-                    "camerax.core.imageCapture.captureMode", ImageCapture.CaptureMode.class);
+                    "camerax.core.imageCapture.captureMode", CaptureMode.class);
     static final Option<FlashMode> OPTION_FLASH_MODE =
             Option.create("camerax.core.imageCapture.flashMode", FlashMode.class);
     static final Option<CaptureBundle> OPTION_CAPTURE_BUNDLE =
@@ -51,6 +51,8 @@
             Option.create("camerax.core.imageCapture.captureProcessor", CaptureProcessor.class);
     static final Option<Integer> OPTION_BUFFER_FORMAT =
             Option.create("camerax.core.imageCapture.bufferFormat", Integer.class);
+    static final Option<Integer> OPTION_MAX_CAPTURE_STAGES =
+            Option.create("camerax.core.imageCapture.maxCaptureStages", Integer.class);
 
     // *********************************************************************************************
 
@@ -62,25 +64,25 @@
     }
 
     /**
-     * Returns the {@link ImageCapture.CaptureMode}.
+     * Returns the {@link CaptureMode}.
      *
      * @param valueIfMissing The value to return if this configuration option has not been set.
      * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
      * configuration.
      */
     @Nullable
-    public ImageCapture.CaptureMode getCaptureMode(
-            @Nullable ImageCapture.CaptureMode valueIfMissing) {
+    public CaptureMode getCaptureMode(
+            @Nullable CaptureMode valueIfMissing) {
         return retrieveOption(OPTION_IMAGE_CAPTURE_MODE, valueIfMissing);
     }
 
     /**
-     * Returns the {@link ImageCapture.CaptureMode}.
+     * Returns the {@link CaptureMode}.
      *
      * @return The stored value, if it exists in this configuration.
      * @throws IllegalArgumentException if the option does not exist in this configuration.
      */
-    public ImageCapture.CaptureMode getCaptureMode() {
+    public CaptureMode getCaptureMode() {
         return retrieveOption(OPTION_IMAGE_CAPTURE_MODE);
     }
 
@@ -185,6 +187,32 @@
         return retrieveOption(OPTION_BUFFER_FORMAT);
     }
 
+    /**
+     * Returns the max number of {@link CaptureStage}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in
+     * this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public int getMaxCaptureStages(int valueIfMissing) {
+        return retrieveOption(OPTION_MAX_CAPTURE_STAGES, valueIfMissing);
+    }
+
+    /**
+     * Returns the max number of {@link CaptureStage}.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getMaxCaptureStages() {
+        return retrieveOption(OPTION_MAX_CAPTURE_STAGES);
+    }
+
     // Start of the default implementation of Config
     // *********************************************************************************************
 
@@ -486,16 +514,33 @@
         return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
     }
 
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener(
+            @Nullable UseCase.EventListener valueIfMissing) {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener() {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER);
+    }
+
     // End of the default implementation of Config
     // *********************************************************************************************
 
     /** Builder for a {@link ImageCaptureConfig}. */
     public static final class Builder
             implements UseCaseConfig.Builder<
-            ImageCapture, ImageCaptureConfig, ImageCaptureConfig.Builder>,
-            ImageOutputConfig.Builder<ImageCaptureConfig.Builder>,
-            CameraDeviceConfig.Builder<ImageCaptureConfig.Builder>,
-            ThreadConfig.Builder<ImageCaptureConfig.Builder> {
+            ImageCapture, ImageCaptureConfig, Builder>,
+            ImageOutputConfig.Builder<Builder>,
+            CameraDeviceConfig.Builder<Builder>,
+            ThreadConfig.Builder<Builder> {
 
         private final MutableOptionsBundle mMutableConfig;
 
@@ -560,7 +605,7 @@
          * @param captureMode The requested image capture mode.
          * @return The current Builder.
          */
-        public Builder setCaptureMode(ImageCapture.CaptureMode captureMode) {
+        public Builder setCaptureMode(CaptureMode captureMode) {
             getMutableConfig().insertOption(OPTION_IMAGE_CAPTURE_MODE, captureMode);
             return this;
         }
@@ -622,6 +667,19 @@
             return this;
         }
 
+        /**
+         * Sets the max number of {@link CaptureStage}.
+         *
+         * @param maxCaptureStages The max CaptureStage number.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setMaxCaptureStages(int maxCaptureStages) {
+            getMutableConfig().insertOption(OPTION_MAX_CAPTURE_STAGES, maxCaptureStages);
+            return this;
+        }
+
         // Implementations of TargetConfig.Builder default methods
 
         /** @hide */
@@ -772,5 +830,14 @@
             getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setUseCaseEventListener(UseCase.EventListener useCaseEventListener) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_LISTENER, useCaseEventListener);
+            return this;
+        }
+
     }
 }
diff --git a/camera/core/src/main/java/androidx/camera/core/MultiValueSet.java b/camera/core/src/main/java/androidx/camera/core/MultiValueSet.java
new file mode 100644
index 0000000..205a2cb
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MultiValueSet.java
@@ -0,0 +1,63 @@
+/*
+ * 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.camera.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A value set implementation that store multiple values in type C.
+ *
+ * @param <C> The type of the parameter.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class MultiValueSet<C> {
+
+    private Set<C> mSet = new HashSet<>();
+
+    /**
+     * Adds all of the elements in the specified collection to this value set if they're not already
+     * present (optional operation).
+     *
+     * @param  value collection containing elements to be added to this value.
+     */
+    public void addAll(List<C> value) {
+        mSet.addAll(value);
+    }
+
+    /**
+     * Returns the list of {@link C} which containing all the elements were added to this value set.
+     */
+    @NonNull
+    public List<C> getAllItems() {
+        return Collections.unmodifiableList(new ArrayList<>(mSet));
+    }
+
+    /**
+     * Need to implement the clone method for object copy.
+     * @return the cloned instance.
+     */
+    public abstract MultiValueSet<C> clone();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/PreviewConfig.java b/camera/core/src/main/java/androidx/camera/core/PreviewConfig.java
index 8514de2..7762f378 100644
--- a/camera/core/src/main/java/androidx/camera/core/PreviewConfig.java
+++ b/camera/core/src/main/java/androidx/camera/core/PreviewConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -343,15 +343,32 @@
         return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
     }
 
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener(
+            @Nullable UseCase.EventListener valueIfMissing) {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener() {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER);
+    }
+
     // End of the default implementation of Config
     // *********************************************************************************************
 
     /** Builder for a {@link PreviewConfig}. */
     public static final class Builder
-            implements UseCaseConfig.Builder<Preview, PreviewConfig, PreviewConfig.Builder>,
-            ImageOutputConfig.Builder<PreviewConfig.Builder>,
-            CameraDeviceConfig.Builder<PreviewConfig.Builder>,
-            ThreadConfig.Builder<PreviewConfig.Builder> {
+            implements UseCaseConfig.Builder<Preview, PreviewConfig, Builder>,
+            ImageOutputConfig.Builder<Builder>,
+            CameraDeviceConfig.Builder<Builder>,
+            ThreadConfig.Builder<Builder> {
 
         private final MutableOptionsBundle mMutableConfig;
 
@@ -559,5 +576,13 @@
             return this;
         }
 
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setUseCaseEventListener(UseCase.EventListener useCaseEventListener) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_LISTENER, useCaseEventListener);
+            return this;
+        }
+
     }
 }
diff --git a/camera/core/src/main/java/androidx/camera/core/SessionConfig.java b/camera/core/src/main/java/androidx/camera/core/SessionConfig.java
index 638e049..1bdd9c1 100644
--- a/camera/core/src/main/java/androidx/camera/core/SessionConfig.java
+++ b/camera/core/src/main/java/androidx/camera/core/SessionConfig.java
@@ -344,6 +344,11 @@
             mCaptureConfigBuilder.setImplementationOptions(config);
         }
 
+        /** Add a set of {@link Config} to the implementation specific options. */
+        public void addImplementationOptions(Config config) {
+            mCaptureConfigBuilder.addImplementationOptions(config);
+        }
+
         /**
          * Builds an instance of a SessionConfig that has all the combined parameters of the
          * SessionConfig that have been added to the Builder.
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCase.java b/camera/core/src/main/java/androidx/camera/core/UseCase.java
index 3962b40..3259e01 100644
--- a/camera/core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/core/src/main/java/androidx/camera/core/UseCase.java
@@ -330,6 +330,11 @@
     /** Clears internal state of this use case. */
     @CallSuper
     protected void clear() {
+        EventListener eventListener = mUseCaseConfig.getUseCaseEventListener(null);
+        if (eventListener != null) {
+            eventListener.onUnbind();
+        }
+
         mListeners.clear();
     }
 
@@ -407,6 +412,20 @@
     }
 
     /**
+     * Called when use case is binding to life cycle via
+     * {@link CameraX#bindToLifecycle(LifecycleOwner, UseCase...)}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected void onBind() {
+        EventListener eventListener = mUseCaseConfig.getUseCaseEventListener(null);
+        if (eventListener != null) {
+            eventListener.onBind(getCameraIdUnchecked());
+        }
+    }
+
+    /**
      * Retrieves a previously attached {@link CameraControl}.
      *
      * @hide
@@ -484,4 +503,39 @@
          */
         void onUseCaseReset(UseCase useCase);
     }
+
+    /**
+     * Listener called when a {@link UseCase} transitions between bind/unbind states.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public interface EventListener {
+
+        /**
+         * Called when use case was bound to the life cycle.
+         * @param cameraId that current used.
+         */
+        void onBind(String cameraId);
+
+        /**
+         * Called when use case was unbind from the life cycle and clear the resource of the use
+         * case.
+         */
+        void onUnbind();
+    }
+
+    private String getCameraIdUnchecked() {
+        String cameraId = null;
+        LensFacing lensFacing = mUseCaseConfig.retrieveOption(
+                CameraDeviceConfig.OPTION_LENS_FACING);
+        try {
+            cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Invalid camera lens facing: " + lensFacing, e);
+        }
+
+        return cameraId;
+    }
+
 }
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseConfig.java b/camera/core/src/main/java/androidx/camera/core/UseCaseConfig.java
index 3f97c86c..11c2b4c 100644
--- a/camera/core/src/main/java/androidx/camera/core/UseCaseConfig.java
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseConfig.java
@@ -28,7 +28,8 @@
  * @hide
  */
 @RestrictTo(Scope.LIBRARY_GROUP)
-public interface UseCaseConfig<T extends UseCase> extends TargetConfig<T>, Config {
+public interface UseCaseConfig<T extends UseCase> extends TargetConfig<T>, Config,
+        UseCaseEventConfig {
     // Option Declarations:
     // *********************************************************************************************
 
@@ -156,7 +157,7 @@
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     interface Builder<T extends UseCase, C extends UseCaseConfig<T>, B> extends
-            TargetConfig.Builder<T, B>, ExtendableBuilder {
+            TargetConfig.Builder<T, B>, ExtendableBuilder, UseCaseEventConfig.Builder<B> {
 
         /**
          * Sets the default session configuration for this use case.
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseEventConfig.java b/camera/core/src/main/java/androidx/camera/core/UseCaseEventConfig.java
new file mode 100644
index 0000000..9475a9b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseEventConfig.java
@@ -0,0 +1,76 @@
+/*
+ * 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.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/** Configuration containing options pertaining to EventListener object. */
+public interface UseCaseEventConfig {
+
+    /**
+     * Option: camerax.core.useCaseEventListener
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    Config.Option<UseCase.EventListener> OPTION_USE_CASE_EVENT_LISTENER =
+            Config.Option.create("camerax.core.useCaseEventListener", UseCase.EventListener.class);
+
+    /**
+     * Returns the EventListener.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    UseCase.EventListener getUseCaseEventListener(@Nullable UseCase.EventListener valueIfMissing);
+
+    /**
+     * Returns the EventListener.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    UseCase.EventListener getUseCaseEventListener();
+
+    // Option Declarations:
+    // *********************************************************************************************
+
+    /**
+     * Builder for a {@link UseCaseEventConfig}.
+     *
+     * @param <B> The top level builder type for which this builder is composed with.
+     */
+    interface Builder<B> {
+
+        /**
+         * Sets the EventListener.
+         *
+         * @param eventListener The EventListener.
+         * @return the current Builder.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        B setUseCaseEventListener(UseCase.EventListener eventListener);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/VideoCaptureConfig.java b/camera/core/src/main/java/androidx/camera/core/VideoCaptureConfig.java
index cf99ba8..bd01d0f 100644
--- a/camera/core/src/main/java/androidx/camera/core/VideoCaptureConfig.java
+++ b/camera/core/src/main/java/androidx/camera/core/VideoCaptureConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -547,16 +547,33 @@
         return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
     }
 
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener(
+            @Nullable UseCase.EventListener valueIfMissing) {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener() {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER);
+    }
+
     // End of the default implementation of Config
     // *********************************************************************************************
 
     /** Builder for a {@link VideoCaptureConfig}. */
     public static final class Builder
             implements
-            UseCaseConfig.Builder<VideoCapture, VideoCaptureConfig, VideoCaptureConfig.Builder>,
-            ImageOutputConfig.Builder<VideoCaptureConfig.Builder>,
-            CameraDeviceConfig.Builder<VideoCaptureConfig.Builder>,
-            ThreadConfig.Builder<VideoCaptureConfig.Builder> {
+            UseCaseConfig.Builder<VideoCapture, VideoCaptureConfig, Builder>,
+            ImageOutputConfig.Builder<Builder>,
+            CameraDeviceConfig.Builder<Builder>,
+            ThreadConfig.Builder<Builder> {
 
         private final MutableOptionsBundle mMutableConfig;
 
@@ -849,5 +866,13 @@
             getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setUseCaseEventListener(UseCase.EventListener useCaseEventListener) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_LISTENER, useCaseEventListener);
+            return this;
+        }
     }
 }
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index 185e83c..d30fc28 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -15,6 +15,7 @@
  */
 package androidx.camera.extensions.impl;
 
+import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.TotalCaptureResult;
@@ -23,6 +24,7 @@
 import android.os.Build;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Size;
 import android.view.Surface;
 
 import java.nio.ByteBuffer;
@@ -39,6 +41,7 @@
 public final class BokehImageCaptureExtenderImpl implements ImageCaptureExtenderImpl {
     private static final String TAG = "BokehICExtender";
     private static final int DEFAULT_STAGE_ID = 0;
+    private static final int SESSION_STAGE_ID = 101;
 
     public BokehImageCaptureExtenderImpl() {
     }
@@ -109,7 +112,67 @@
 
                         Log.d(TAG, "Completed bokeh CaptureProcessor");
                     }
+
+                    @Override
+                    public void onResolutionUpdate(Size size) {
+
+                    }
+
+                    @Override
+                    public void onImageFormatUpdate(int imageFormat) {
+
+                    }
                 };
         return captureProcessor;
     }
+
+    @Override
+    public void onInit(String cameraId, CameraCharacteristics cameraCharacteristics,
+            Context context) {
+
+    }
+
+    @Override
+    public void onDeInit() {
+
+    }
+
+    @Override
+    public CaptureStageImpl onPresetSession() {
+        // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
+        // placeholder set of CaptureRequest.Key values
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+
+        return captureStage;
+    }
+
+    @Override
+    public CaptureStageImpl onEnableSession() {
+        // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
+        // placeholder set of CaptureRequest.Key values
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+
+        return captureStage;
+    }
+
+    @Override
+    public CaptureStageImpl onDisableSession() {
+        // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
+        // placeholder set of CaptureRequest.Key values
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+
+        return captureStage;
+    }
+
+    @Override
+    public int getMaxCaptureStage() {
+        return 3;
+    }
+
 }
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index aa3516a..694b511 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -15,6 +15,7 @@
  */
 package androidx.camera.extensions.impl;
 
+import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 
@@ -26,6 +27,7 @@
  */
 public final class BokehPreviewExtenderImpl implements PreviewExtenderImpl {
     private static final int DEFAULT_STAGE_ID = 0;
+    private static final int SESSION_STAGE_ID = 101;
 
     public BokehPreviewExtenderImpl() {}
 
@@ -60,4 +62,48 @@
     public RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor() {
         return RequestUpdateProcessorImpls.noUpdateProcessor();
     }
+
+    @Override
+    public void onInit(String cameraId, CameraCharacteristics cameraCharacteristics,
+            Context context) {
+
+    }
+
+    @Override
+    public void onDeInit() {
+
+    }
+
+    @Override
+    public CaptureStageImpl onPresetSession() {
+        // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
+        // placeholder set of CaptureRequest.Key values
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+
+        return captureStage;
+    }
+
+    @Override
+    public CaptureStageImpl onEnableSession() {
+        // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
+        // placeholder set of CaptureRequest.Key values
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+
+        return captureStage;
+    }
+
+    @Override
+    public CaptureStageImpl onDisableSession() {
+        // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
+        // placeholder set of CaptureRequest.Key values
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+
+        return captureStage;
+    }
 }
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index 04c7247..0b8bdd5 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -19,6 +19,7 @@
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 import android.util.Pair;
+import android.util.Size;
 import android.view.Surface;
 
 import java.util.Map;
@@ -46,4 +47,22 @@
      *                so no references to them should be kept.
      */
     void process(Map<Integer, Pair<Image, TotalCaptureResult>> results);
+
+    /**
+     * This callback will be invoked when CameraX changes the configured input resolution. After
+     * this call, {@link CaptureProcessorImpl} should expect any {@link Image} received as input
+     * to be at the specified resolution.
+     *
+     * @param size for the surface.
+     */
+    void onResolutionUpdate(Size size);
+
+    /**
+     * This callback will be invoked when CameraX changes the configured input image format.
+     * After this call, {@link CaptureProcessorImpl} should expect any {@link Image} received as
+     * input to have the specified image format.
+     *
+     * @param imageFormat for the surface.
+     */
+    void onImageFormatUpdate(int imageFormat);
 }
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
new file mode 100644
index 0000000..5bacab7
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
@@ -0,0 +1,79 @@
+/*
+ * 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.camera.extensions.impl;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+/**
+ * Provides interfaces that the OEM needs to implement to handle the state change.
+ */
+public interface ExtenderStateListener {
+
+    /**
+     * Notify to initialize the extension. This will be called after bindToLifeCycle. This is
+     * where the use case is started and would be able to allocate resources here. After onInit() is
+     * called, the camera ID, cameraCharacteristics and context will not change until onDeInit()
+     * has been called.
+     *
+     * @param cameraId The camera2 id string of the camera.
+     * @param cameraCharacteristics The {@link CameraCharacteristics} of the camera.
+     * @param context The {@link Context} used for CameraX.
+     */
+    void onInit(String cameraId, CameraCharacteristics cameraCharacteristics, Context context);
+
+    /**
+     * Notify to de-initialize the extension. This callback will be invoked after unbind.
+     * After onDeInit() was called, it is expected that the camera ID, cameraCharacteristics will
+     * no longer hold, this should be where to clear all resources allocated for this use case.
+     */
+    void onDeInit();
+
+    /**
+     * This will be invoked before creating a
+     * {@link android.hardware.camera2.CameraCaptureSession}. The {@link CaptureRequest}
+     * parameters returned via {@link CaptureStageImpl} will be passed to the camera device as
+     * part of the capture session initialization via setSessionParameters(). The valid parameter
+     * is a subset of the available capture request parameters.
+     *
+     * @return The request information to set the session wide camera parameters.
+     */
+    CaptureStageImpl onPresetSession();
+
+    /**
+     * This will be invoked once after the {@link android.hardware.camera2.CameraCaptureSession}
+     * has been created. The {@link CaptureRequest} parameters returned via
+     * {@link CaptureStageImpl} will be used to generate a single request to the current
+     * configured {@link CameraDevice}. The generated request will be submitted to camera before
+     * processing other single requests.
+     *
+     * @return The request information to create a single capture request to camera device.
+     */
+    CaptureStageImpl onEnableSession();
+
+    /**
+     * This will be invoked before the {@link android.hardware.camera2.CameraCaptureSession} is
+     * closed. The {@link CaptureRequest} parameters returned via {@link CaptureStageImpl} will
+     * be used to generate a single request to the currently configured {@link CameraDevice}. The
+     * generated request will be submitted to camera before the CameraCaptureSession is closed.
+     *
+     * @return The request information to customize the session.
+     */
+    CaptureStageImpl onDisableSession();
+}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index 96df4c2..ca7b143 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -15,6 +15,7 @@
  */
 package androidx.camera.extensions.impl;
 
+import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.TotalCaptureResult;
@@ -23,6 +24,7 @@
 import android.os.Build;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Size;
 import android.view.Surface;
 
 import java.nio.ByteBuffer;
@@ -42,6 +44,7 @@
     private static final int UNDER_STAGE_ID = 0;
     private static final int NORMAL_STAGE_ID = 1;
     private static final int OVER_STAGE_ID = 2;
+    private static final int SESSION_STAGE_ID = 101;
 
     public HdrImageCaptureExtenderImpl() {
     }
@@ -146,7 +149,52 @@
 
                         Log.d(TAG, "Completed HDR CaptureProcessor");
                     }
+
+                    @Override
+                    public void onResolutionUpdate(Size size) {
+
+                    }
+
+                    @Override
+                    public void onImageFormatUpdate(int imageFormat) {
+
+                    }
                 };
         return captureProcessor;
     }
+
+    @Override
+    public void onInit(String cameraId, CameraCharacteristics cameraCharacteristics,
+            Context context) {
+
+    }
+
+    @Override
+    public void onDeInit() {
+
+    }
+
+    @Override
+    public CaptureStageImpl onPresetSession() {
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        return captureStage;
+    }
+
+    @Override
+    public CaptureStageImpl onEnableSession() {
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        return captureStage;
+    }
+
+    @Override
+    public CaptureStageImpl onDisableSession() {
+        SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
+        return captureStage;
+    }
+
+    @Override
+    public int getMaxCaptureStage() {
+        return 4;
+    }
+
 }
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
index 4f3cf65..f33b561 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.extensions.impl;
 
+import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 
@@ -47,7 +48,7 @@
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(DEFAULT_STAGE_ID);
         captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
-                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+                CaptureRequest.CONTROL_EFFECT_MODE_AQUA);
 
         return captureStage;
     }
@@ -61,4 +62,30 @@
     public RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor() {
         return RequestUpdateProcessorImpls.noUpdateProcessor();
     }
+
+    @Override
+    public void onInit(String cameraId, CameraCharacteristics cameraCharacteristics,
+            Context context) {
+
+    }
+
+    @Override
+    public void onDeInit() {
+
+    }
+
+    @Override
+    public CaptureStageImpl onPresetSession() {
+        return null;
+    }
+
+    @Override
+    public CaptureStageImpl onEnableSession() {
+        return null;
+    }
+
+    @Override
+    public CaptureStageImpl onDisableSession() {
+        return null;
+    }
 }
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
index 94d40d5..a54ead2 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
@@ -23,7 +23,7 @@
 /**
  * Provides abstract methods that the OEM needs to implement to enable extensions for image capture.
  */
-public interface ImageCaptureExtenderImpl {
+public interface ImageCaptureExtenderImpl extends ExtenderStateListener {
     /**
      * Indicates whether the extension is supported on the device.
      *
@@ -41,12 +41,18 @@
      */
     void enableExtension(String cameraId, CameraCharacteristics cameraCharacteristics);
 
-    /** The set of captures that are needed to create an image with the effect. */
-    List<CaptureStageImpl> getCaptureStages();
-
     /**
      * The processing that will be done on a set of captures to create and image with the effect.
      */
     CaptureProcessorImpl getCaptureProcessor();
+
+    /** The set of captures that are needed to create an image with the effect. */
+    List<CaptureStageImpl> getCaptureStages();
+
+    /**
+     * Returns the maximum size of the list returned by {@link #getCaptureStages()}.
+     * @return the maximum count.
+     */
+    int getMaxCaptureStage();
 }
 
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
index 9402782..79ff4db 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
@@ -21,7 +21,7 @@
 /**
  * Provides abstract methods that the OEM needs to implement to enable extensions in the preview.
  */
-public interface PreviewExtenderImpl {
+public interface PreviewExtenderImpl extends ExtenderStateListener {
     /** The different types of the preview processing. */
     enum ProcessorType {
         /** Processing which only updates the {@link CaptureStageImpl}. */
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
index f51bb3a..35aaab7 100644
--- a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
@@ -24,6 +24,7 @@
 import androidx.camera.core.MutableOptionsBundle;
 import androidx.camera.core.OptionsBundle;
 import androidx.camera.core.SessionConfig;
+import androidx.camera.core.UseCase;
 import androidx.camera.core.UseCaseConfig;
 
 import java.util.Set;
@@ -152,6 +153,19 @@
         return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
     }
 
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener(
+            @Nullable UseCase.EventListener valueIfMissing) {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER, valueIfMissing);
+    }
+
+    @Nullable
+    @Override
+    public UseCase.EventListener getUseCaseEventListener() {
+        return retrieveOption(OPTION_USE_CASE_EVENT_LISTENER);
+    }
+
     // End of the default implementation of Config
     // *********************************************************************************************
 
@@ -227,5 +241,11 @@
             getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
             return this;
         }
+
+        @Override
+        public Builder setUseCaseEventListener(UseCase.EventListener eventListener) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_LISTENER, eventListener);
+            return this;
+        }
     }
 }
diff --git a/collection/api/1.2.0-alpha01.txt b/collection/api/1.2.0-alpha01.txt
new file mode 100644
index 0000000..179d17f
--- /dev/null
+++ b/collection/api/1.2.0-alpha01.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+  public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+    ctor public ArrayMap();
+    ctor public ArrayMap(int);
+    ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+    method public boolean containsAll(java.util.Collection<?>);
+    method public java.util.Set<java.util.Map.Entry<K,V>>! entrySet();
+    method public java.util.Set<K>! keySet();
+    method public void putAll(java.util.Map<? extends K,? extends V>!);
+    method public boolean removeAll(java.util.Collection<?>);
+    method public boolean retainAll(java.util.Collection<?>);
+    method public java.util.Collection<V>! values();
+  }
+
+  public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+    ctor public ArraySet();
+    ctor public ArraySet(int);
+    ctor public ArraySet(androidx.collection.ArraySet<E>?);
+    ctor public ArraySet(java.util.Collection<E>?);
+    method public boolean add(E?);
+    method public void addAll(androidx.collection.ArraySet<? extends E>);
+    method public boolean addAll(java.util.Collection<? extends E>);
+    method public void clear();
+    method public boolean contains(Object?);
+    method public boolean containsAll(java.util.Collection<?>);
+    method public void ensureCapacity(int);
+    method public int indexOf(Object?);
+    method public boolean isEmpty();
+    method public java.util.Iterator<E>! iterator();
+    method public boolean remove(Object?);
+    method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+    method public boolean removeAll(java.util.Collection<?>);
+    method public E! removeAt(int);
+    method public boolean retainAll(java.util.Collection<?>);
+    method public int size();
+    method public Object[] toArray();
+    method public <T> T[] toArray(T[]);
+    method public E? valueAt(int);
+  }
+
+  public final class CircularArray<E> {
+    ctor public CircularArray();
+    ctor public CircularArray(int);
+    method public void addFirst(E!);
+    method public void addLast(E!);
+    method public void clear();
+    method public E! get(int);
+    method public E! getFirst();
+    method public E! getLast();
+    method public boolean isEmpty();
+    method public E! popFirst();
+    method public E! popLast();
+    method public void removeFromEnd(int);
+    method public void removeFromStart(int);
+    method public int size();
+  }
+
+  public final class CircularIntArray {
+    ctor public CircularIntArray();
+    ctor public CircularIntArray(int);
+    method public void addFirst(int);
+    method public void addLast(int);
+    method public void clear();
+    method public int get(int);
+    method public int getFirst();
+    method public int getLast();
+    method public boolean isEmpty();
+    method public int popFirst();
+    method public int popLast();
+    method public void removeFromEnd(int);
+    method public void removeFromStart(int);
+    method public int size();
+  }
+
+  public class LongSparseArray<E> implements java.lang.Cloneable {
+    ctor public LongSparseArray();
+    ctor public LongSparseArray(int);
+    method public void append(long, E!);
+    method public void clear();
+    method public androidx.collection.LongSparseArray<E>! clone();
+    method public boolean containsKey(long);
+    method public boolean containsValue(E!);
+    method @Deprecated public void delete(long);
+    method public E? get(long);
+    method public E! get(long, E!);
+    method public int indexOfKey(long);
+    method public int indexOfValue(E!);
+    method public boolean isEmpty();
+    method public long keyAt(int);
+    method public void put(long, E!);
+    method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+    method public E? putIfAbsent(long, E!);
+    method public void remove(long);
+    method public boolean remove(long, Object!);
+    method public void removeAt(int);
+    method public E? replace(long, E!);
+    method public boolean replace(long, E!, E!);
+    method public void setValueAt(int, E!);
+    method public int size();
+    method public E! valueAt(int);
+  }
+
+  public class LruCache<K, V> {
+    ctor public LruCache(int);
+    method protected V? create(K);
+    method public final int createCount();
+    method protected void entryRemoved(boolean, K, V, V?);
+    method public final void evictAll();
+    method public final int evictionCount();
+    method public final V? get(K);
+    method public final int hitCount();
+    method public final int maxSize();
+    method public final int missCount();
+    method public final V? put(K, V);
+    method public final int putCount();
+    method public final V? remove(K);
+    method public void resize(int);
+    method public final int size();
+    method protected int sizeOf(K, V);
+    method public final java.util.Map<K,V>! snapshot();
+    method public final String toString();
+    method public void trimToSize(int);
+  }
+
+  public class SimpleArrayMap<K, V> {
+    ctor public SimpleArrayMap();
+    ctor public SimpleArrayMap(int);
+    ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K,V>!);
+    method public void clear();
+    method public boolean containsKey(Object?);
+    method public boolean containsValue(Object!);
+    method public void ensureCapacity(int);
+    method public V? get(Object!);
+    method public V! getOrDefault(Object!, V!);
+    method public int indexOfKey(Object?);
+    method public boolean isEmpty();
+    method public K! keyAt(int);
+    method public V? put(K!, V!);
+    method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+    method public V? putIfAbsent(K!, V!);
+    method public V? remove(Object!);
+    method public boolean remove(Object!, Object!);
+    method public V! removeAt(int);
+    method public V? replace(K!, V!);
+    method public boolean replace(K!, V!, V!);
+    method public V! setValueAt(int, V!);
+    method public int size();
+    method public V! valueAt(int);
+  }
+
+  public class SparseArrayCompat<E> implements java.lang.Cloneable {
+    ctor public SparseArrayCompat();
+    ctor public SparseArrayCompat(int);
+    method public void append(int, E!);
+    method public void clear();
+    method public androidx.collection.SparseArrayCompat<E>! clone();
+    method public boolean containsKey(int);
+    method public boolean containsValue(E!);
+    method @Deprecated public void delete(int);
+    method public E? get(int);
+    method public E! get(int, E!);
+    method public int indexOfKey(int);
+    method public int indexOfValue(E!);
+    method public boolean isEmpty();
+    method public int keyAt(int);
+    method public void put(int, E!);
+    method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+    method public E? putIfAbsent(int, E!);
+    method public void remove(int);
+    method public boolean remove(int, Object!);
+    method public void removeAt(int);
+    method public void removeAtRange(int, int);
+    method public E? replace(int, E!);
+    method public boolean replace(int, E!, E!);
+    method public void setValueAt(int, E!);
+    method public int size();
+    method public E! valueAt(int);
+  }
+
+}
+
diff --git a/collection/api/restricted_1.2.0-alpha01.txt b/collection/api/restricted_1.2.0-alpha01.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/collection/api/restricted_1.2.0-alpha01.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/collection/ktx/api/1.2.0-alpha01.txt b/collection/ktx/api/1.2.0-alpha01.txt
new file mode 100644
index 0000000..3fe6a36
--- /dev/null
+++ b/collection/ktx/api/1.2.0-alpha01.txt
@@ -0,0 +1,52 @@
+// Signature format: 3.0
+package androidx.collection {
+
+  public final class ArrayMapKt {
+    ctor public ArrayMapKt();
+    method public static inline <K, V> androidx.collection.ArrayMap<K,V> arrayMapOf();
+    method public static <K, V> androidx.collection.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
+  }
+
+  public final class ArraySetKt {
+    ctor public ArraySetKt();
+    method public static inline <T> androidx.collection.ArraySet<T> arraySetOf();
+    method public static <T> androidx.collection.ArraySet<T> arraySetOf(T... values);
+  }
+
+  public final class LongSparseArrayKt {
+    ctor public LongSparseArrayKt();
+    method public static inline operator <T> boolean contains(androidx.collection.LongSparseArray<T>, long key);
+    method public static inline <T> void forEach(androidx.collection.LongSparseArray<T>, kotlin.jvm.functions.Function2<? super java.lang.Long,? super T,kotlin.Unit> action);
+    method public static inline <T> T! getOrDefault(androidx.collection.LongSparseArray<T>, long key, T! defaultValue);
+    method public static inline <T> T! getOrElse(androidx.collection.LongSparseArray<T>, long key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method public static inline <T> int getSize(androidx.collection.LongSparseArray<T>);
+    method public static inline <T> boolean isNotEmpty(androidx.collection.LongSparseArray<T>);
+    method public static <T> kotlin.collections.LongIterator keyIterator(androidx.collection.LongSparseArray<T>);
+    method public static operator <T> androidx.collection.LongSparseArray<T> plus(androidx.collection.LongSparseArray<T>, androidx.collection.LongSparseArray<T> other);
+    method @Deprecated public static <T> boolean remove(androidx.collection.LongSparseArray<T>, long key, T! value);
+    method public static inline operator <T> void set(androidx.collection.LongSparseArray<T>, long key, T! value);
+    method public static <T> java.util.Iterator<T> valueIterator(androidx.collection.LongSparseArray<T>);
+  }
+
+  public final class LruCacheKt {
+    ctor public LruCacheKt();
+    method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf = { _, _ -> 1 }, kotlin.jvm.functions.Function1<? super K,? extends V> create = { (V)null }, kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit>  _, _, _, _ ->  });
+  }
+
+  public final class SparseArrayKt {
+    ctor public SparseArrayKt();
+    method public static inline operator <T> boolean contains(androidx.collection.SparseArrayCompat<T>, int key);
+    method public static inline <T> void forEach(androidx.collection.SparseArrayCompat<T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
+    method public static inline <T> T! getOrDefault(androidx.collection.SparseArrayCompat<T>, int key, T! defaultValue);
+    method public static inline <T> T! getOrElse(androidx.collection.SparseArrayCompat<T>, int key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+    method public static inline <T> int getSize(androidx.collection.SparseArrayCompat<T>);
+    method public static inline <T> boolean isNotEmpty(androidx.collection.SparseArrayCompat<T>);
+    method public static <T> kotlin.collections.IntIterator keyIterator(androidx.collection.SparseArrayCompat<T>);
+    method public static operator <T> androidx.collection.SparseArrayCompat<T> plus(androidx.collection.SparseArrayCompat<T>, androidx.collection.SparseArrayCompat<T> other);
+    method @Deprecated public static <T> boolean remove(androidx.collection.SparseArrayCompat<T>, int key, T! value);
+    method public static inline operator <T> void set(androidx.collection.SparseArrayCompat<T>, int key, T! value);
+    method public static <T> java.util.Iterator<T> valueIterator(androidx.collection.SparseArrayCompat<T>);
+  }
+
+}
+
diff --git a/collection/ktx/api/restricted_1.2.0-alpha01.txt b/collection/ktx/api/restricted_1.2.0-alpha01.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/collection/ktx/api/restricted_1.2.0-alpha01.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/collection/ktx/build.gradle b/collection/ktx/build.gradle
index e813c94..3b51b3d 100644
--- a/collection/ktx/build.gradle
+++ b/collection/ktx/build.gradle
@@ -24,7 +24,7 @@
 }
 
 dependencies {
-    compile(project(":collection"))
+    compile("androidx.collection:collection:1.1.0-rc01")
     compile(KOTLIN_STDLIB)
     testCompile(JUNIT)
     testCompile(TRUTH)
diff --git a/compose/README.md b/compose/README.md
index 0cc9154..be48786 100644
--- a/compose/README.md
+++ b/compose/README.md
@@ -2,7 +2,7 @@
 
 Jetpack Compose makes it easy to write and manage an application's frontend by providing a declarative API that allows users to update their Android application UI without imperatively mutating frontend views.
 
-A Compose application is comprised of `@Composable` functions, which are [pure functions](https://en.wikipedia.org/wiki/Pure_function) that transform application data into a UI hierarchy.  When the underlying data changes, the Composable functions can be re-invoked to generate an updated UI hierarchy.
+A Compose application is comprised of `@Composable` functions, which are functions that transform application data into a UI hierarchy.  When the underlying data changes, the Composable functions can be re-invoked to generate an updated UI hierarchy.
 
 ```
 import androidx.compose.*
diff --git a/content/build.gradle b/content/build.gradle
index 1be69c9..91fa5e9 100644
--- a/content/build.gradle
+++ b/content/build.gradle
@@ -26,7 +26,7 @@
 dependencies {
     api("androidx.annotation:annotation:1.1.0-rc01")
     api(project(":core"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     androidTestImplementation(JUNIT)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/customview/build.gradle b/customview/build.gradle
index 1a08bcd..738f886 100644
--- a/customview/build.gradle
+++ b/customview/build.gradle
@@ -10,7 +10,7 @@
 dependencies {
     api("androidx.annotation:annotation:1.1.0-rc01")
     api(project(":core"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     androidTestImplementation(JUNIT)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/dynamic-animation/build.gradle b/dynamic-animation/build.gradle
index 03ceb41..e9d5ab2 100644
--- a/dynamic-animation/build.gradle
+++ b/dynamic-animation/build.gradle
@@ -9,7 +9,7 @@
 
 dependencies {
     api(project(":core"))
-    api(project(":collection"))
+    api("androidx.collection:collection:1.1.0-rc01")
     api(project(":legacy-support-core-utils"))
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/enterprise/feedback/api/1.0.0-alpha02.txt b/enterprise/feedback/api/1.0.0-alpha02.txt
index e85daff..7aa61dc 100644
--- a/enterprise/feedback/api/1.0.0-alpha02.txt
+++ b/enterprise/feedback/api/1.0.0-alpha02.txt
@@ -22,12 +22,9 @@
     method public abstract androidx.enterprise.feedback.KeyedAppState.KeyedAppStateBuilder setSeverity(int);
   }
 
-  public class KeyedAppStatesReporter {
-    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
-    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
-    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    field public static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
+  public abstract class KeyedAppStatesReporter {
+    method public abstract void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public abstract void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
   }
 
   public abstract class KeyedAppStatesService extends android.app.Service {
@@ -56,5 +53,12 @@
     method public abstract androidx.enterprise.feedback.ReceivedKeyedAppState.ReceivedKeyedAppStateBuilder setTimestamp(long);
   }
 
+  public class SingletonKeyedAppStatesReporter extends androidx.enterprise.feedback.KeyedAppStatesReporter {
+    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
+    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
+    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+  }
+
 }
 
diff --git a/enterprise/feedback/api/current.txt b/enterprise/feedback/api/current.txt
index e85daff..7aa61dc 100644
--- a/enterprise/feedback/api/current.txt
+++ b/enterprise/feedback/api/current.txt
@@ -22,12 +22,9 @@
     method public abstract androidx.enterprise.feedback.KeyedAppState.KeyedAppStateBuilder setSeverity(int);
   }
 
-  public class KeyedAppStatesReporter {
-    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
-    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
-    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
-    field public static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
+  public abstract class KeyedAppStatesReporter {
+    method public abstract void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public abstract void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
   }
 
   public abstract class KeyedAppStatesService extends android.app.Service {
@@ -56,5 +53,12 @@
     method public abstract androidx.enterprise.feedback.ReceivedKeyedAppState.ReceivedKeyedAppStateBuilder setTimestamp(long);
   }
 
+  public class SingletonKeyedAppStatesReporter extends androidx.enterprise.feedback.KeyedAppStatesReporter {
+    method public static androidx.enterprise.feedback.KeyedAppStatesReporter getInstance(android.content.Context);
+    method public static void initialize(android.content.Context, java.util.concurrent.Executor);
+    method public void setStates(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+    method public void setStatesImmediate(java.util.Collection<androidx.enterprise.feedback.KeyedAppState>);
+  }
+
 }
 
diff --git a/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java
index 0fea1b5..3dc36fc 100644
--- a/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java
+++ b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/KeyedAppStatesReporter.java
@@ -16,48 +16,27 @@
 
 package androidx.enterprise.feedback;
 
-import android.annotation.SuppressLint;
 import android.app.admin.DevicePolicyManager;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.os.Build;
-import android.os.Bundle;
 import android.os.Message;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 
 /**
  * A reporter of keyed app states to enable communication between an app and an EMM (enterprise
  * mobility management).
+ *
+ * For production use {@link SingletonKeyedAppStatesReporter}.
  */
-public class KeyedAppStatesReporter {
+public abstract class KeyedAppStatesReporter {
 
-    private static final String LOG_TAG = "KeyedAppStatesReporter";
+    // Package-private constructor to restrict subclasses to the same package
+    KeyedAppStatesReporter() {}
 
     static final String PHONESKY_PACKAGE_NAME = "com.android.vending";
 
-    @SuppressLint("StaticFieldLeak") // Application Context only.
-    private static volatile KeyedAppStatesReporter sSingleton;
-
     /** The value of {@link Message#what} to indicate a state update. */
     static final int WHAT_STATE = 1;
 
@@ -99,85 +78,15 @@
     static final String APP_STATE_DATA = "androidx.enterprise.feedback.APP_STATE_DATA";
 
     /** The intent action for reporting app states. */
-    public static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
+    static final String ACTION_APP_STATES = "androidx.enterprise.feedback.action.APP_STATES";
 
-    private final Context mContext;
+    static boolean canPackageReceiveAppStates(Context context, String packageName) {
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
 
-    private final Map<String, BufferedServiceConnection> mServiceConnections = new HashMap<>();
-
-    private static final int EXECUTOR_IDLE_ALIVE_TIME_SECS = 20;
-    private final Executor mExecutor;
-
-    /**
-     * Creates an {@link ExecutorService} which has no persistent background thread, and ensures
-     * tasks will run in submit order.
-     */
-    private static ExecutorService createExecutorService() {
-        return new ThreadPoolExecutor(
-                /* corePoolSize= */ 0,
-                /* maximumPoolSize= */ 1,
-                EXECUTOR_IDLE_ALIVE_TIME_SECS,
-                TimeUnit.SECONDS,
-                new LinkedBlockingQueue<Runnable>() /* Not used */);
-    }
-
-    /**
-     * Sets executor used to construct the singleton.
-     *
-     * <p>If required, this method must be called before calling {@link #getInstance(Context)}.
-     *
-     * <p>If this method is not called, the reporter will run on a newly-created thread.
-     * This newly-created thread will be cleaned up and recreated as necessary when idle.
-     */
-    public static void initialize(@NonNull Context context, @NonNull Executor executor) {
-        if (context == null || executor == null) {
-            throw new NullPointerException();
-        }
-        synchronized (KeyedAppStatesReporter.class) {
-            if (sSingleton != null) {
-                throw new IllegalStateException(
-                        "initialize can only be called once and must be called before "
-                            + "calling getInstance.");
-            }
-            initializeSingleton(context, executor);
-        }
-    }
-
-    /**
-     * Returns an instance of the reporter.
-     *
-     * <p>Creates and initializes an instance if one doesn't already exist.
-     */
-    @NonNull
-    public static KeyedAppStatesReporter getInstance(@NonNull Context context) {
-        if (context == null || context.getApplicationContext() == null) {
-            throw new NullPointerException();
-        }
-        if (sSingleton == null) {
-            synchronized (KeyedAppStatesReporter.class) {
-                if (sSingleton == null) {
-                    initializeSingleton(context, createExecutorService());
-                }
-            }
-        }
-        return sSingleton;
-    }
-
-    private static void initializeSingleton(@NonNull Context context, @NonNull Executor executor) {
-        sSingleton = new KeyedAppStatesReporter(context, executor);
-        sSingleton.bind();
-    }
-
-    @VisibleForTesting
-    static void resetSingleton() {
-        synchronized (KeyedAppStatesReporter.class) {
-            sSingleton = null;
-        }
-    }
-
-    private KeyedAppStatesReporter(Context context, Executor executor) {
-        this.mContext = context.getApplicationContext();
-        this.mExecutor = executor;
+        return packageName.equals(PHONESKY_PACKAGE_NAME)
+            || devicePolicyManager.isDeviceOwnerApp(packageName)
+            || devicePolicyManager.isProfileOwnerApp(packageName);
     }
 
     /**
@@ -201,25 +110,7 @@
      *
      * @see #setStatesImmediate(Collection)
      */
-    public void setStates(@NonNull Collection<KeyedAppState> states) {
-        setStates(states, false);
-    }
-
-    private void setStates(final Collection<KeyedAppState> states, final boolean immediate) {
-        mExecutor.execute(new Runnable() {
-            @Override
-            public void run() {
-                if (states.isEmpty()) {
-                    return;
-                }
-
-                unbindOldBindings();
-                bind();
-
-                send(buildStatesBundle(states), immediate);
-            }
-        });
-    }
+    public abstract void setStates(@NonNull Collection<KeyedAppState> states);
 
     /**
      * Performs the same function as {@link #setStates(Collection)}, except it
@@ -229,150 +120,5 @@
      * <p>The receiver is not obligated to meet this immediate upload request.
      * For example, Play and Android Management APIs have daily quotas.
      */
-    public void setStatesImmediate(@NonNull Collection<KeyedAppState> states) {
-        setStates(states, true);
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void bind() {
-        Collection<String> acceptablePackageNames = getDeviceOwnerAndProfileOwnerPackageNames();
-        acceptablePackageNames.add(PHONESKY_PACKAGE_NAME);
-        bind(acceptablePackageNames);
-    }
-
-    private void bind(Collection<String> acceptablePackageNames) {
-        // Remove already-bound packages
-        Collection<String> filteredPackageNames = new HashSet<>();
-        for (String packageName : acceptablePackageNames) {
-            if (!mServiceConnections.containsKey(packageName)) {
-                filteredPackageNames.add(packageName);
-            }
-        }
-
-        if (filteredPackageNames.isEmpty()) {
-            return;
-        }
-
-        Collection<ServiceInfo> serviceInfos =
-                getServiceInfoInPackages(new Intent(ACTION_APP_STATES), filteredPackageNames);
-
-        for (ServiceInfo serviceInfo : serviceInfos) {
-            Intent bindIntent = new Intent();
-            bindIntent.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
-
-            BufferedServiceConnection bufferedServiceConnection =
-                    new BufferedServiceConnection(
-                        mExecutor, mContext, bindIntent, Context.BIND_AUTO_CREATE);
-            bufferedServiceConnection.bindService();
-
-            mServiceConnections.put(serviceInfo.packageName, bufferedServiceConnection);
-        }
-    }
-
-    private Collection<String> getDeviceOwnerAndProfileOwnerPackageNames() {
-        DevicePolicyManager devicePolicyManager =
-                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
-        Collection<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
-
-        if (activeAdmins == null) {
-            return new ArrayList<>();
-        }
-
-        Collection<String> deviceOwnerProfileOwnerPackageNames = new ArrayList<>();
-
-        for (ComponentName componentName : activeAdmins) {
-            if (devicePolicyManager.isDeviceOwnerApp(componentName.getPackageName())
-                    || devicePolicyManager.isProfileOwnerApp(componentName.getPackageName())) {
-                deviceOwnerProfileOwnerPackageNames.add(componentName.getPackageName());
-            }
-        }
-
-        return deviceOwnerProfileOwnerPackageNames;
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void unbindOldBindings() {
-        Iterator<Entry<String, BufferedServiceConnection>> iterator =
-                mServiceConnections.entrySet().iterator();
-
-        while (iterator.hasNext()) {
-            Entry<String, BufferedServiceConnection> entry = iterator.next();
-            if (packageNameShouldBeUnbound(entry.getKey())) {
-                entry.getValue().unbind();
-                iterator.remove();
-            }
-        }
-    }
-
-    /** Assumes the given package name is a stored service connection. */
-    private boolean packageNameShouldBeUnbound(String packageName) {
-        if (Build.VERSION.SDK_INT < 26
-                && mServiceConnections.get(packageName).hasBeenDisconnected()) {
-            return true;
-        }
-
-        if (mServiceConnections.get(packageName).isDead()) {
-            return true;
-        }
-
-        if (!canPackageReceiveAppStates(mContext, packageName)) {
-            return true;
-        }
-
-        return false;
-    }
-
-    static boolean canPackageReceiveAppStates(Context context, String packageName) {
-        DevicePolicyManager devicePolicyManager =
-                (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
-
-        return packageName.equals(PHONESKY_PACKAGE_NAME)
-                || devicePolicyManager.isDeviceOwnerApp(packageName)
-                || devicePolicyManager.isProfileOwnerApp(packageName);
-    }
-
-    private Collection<ServiceInfo> getServiceInfoInPackages(
-            Intent intent, Collection<String> acceptablePackageNames) {
-        PackageManager packageManager = mContext.getPackageManager();
-        List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(intent, /* flags = */0);
-
-        Collection<ServiceInfo> validServiceInfo = new ArrayList<>();
-        for (ResolveInfo i : resolveInfos) {
-            if (acceptablePackageNames.contains(i.serviceInfo.packageName)) {
-                validServiceInfo.add(i.serviceInfo);
-            }
-        }
-        return validServiceInfo;
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static Bundle buildStatesBundle(Collection<KeyedAppState> keyedAppStates) {
-        Bundle bundle = new Bundle();
-        bundle.putParcelableArrayList(APP_STATES, buildStateBundles(keyedAppStates));
-        return bundle;
-    }
-
-    // Returns an ArrayList as required to be used with Bundle#putParcelableArrayList.
-    private static ArrayList<Bundle> buildStateBundles(Collection<KeyedAppState> keyedAppStates) {
-        ArrayList<Bundle> bundles = new ArrayList<>();
-        for (KeyedAppState keyedAppState : keyedAppStates) {
-            bundles.add(keyedAppState.toStateBundle());
-        }
-        return bundles;
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void send(Bundle appStatesBundle, boolean immediate) {
-        for (BufferedServiceConnection serviceConnection : mServiceConnections.values()) {
-            // Messages cannot be reused so we create a copy for each service connection.
-            serviceConnection.send(createStateMessage(appStatesBundle, immediate));
-        }
-    }
-
-    private static Message createStateMessage(Bundle appStatesBundle, boolean immediate) {
-        Message message = Message.obtain();
-        message.what = immediate ? WHAT_IMMEDIATE_STATE : WHAT_STATE;
-        message.obj = appStatesBundle;
-        return message;
-    }
+    public abstract void setStatesImmediate(@NonNull Collection<KeyedAppState> states);
 }
diff --git a/enterprise/feedback/src/main/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporter.java b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporter.java
new file mode 100644
index 0000000..fba3232
--- /dev/null
+++ b/enterprise/feedback/src/main/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporter.java
@@ -0,0 +1,297 @@
+/*
+ * 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.enterprise.feedback;
+
+import android.annotation.SuppressLint;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link KeyedAppStatesReporter} that only allows a single instance to exist at one time,
+ * avoiding repeated instantiations.
+ */
+public class SingletonKeyedAppStatesReporter extends KeyedAppStatesReporter {
+
+    private static final String LOG_TAG = "KeyedAppStatesReporter";
+
+    @SuppressLint("StaticFieldLeak") // Application Context only.
+    private static volatile SingletonKeyedAppStatesReporter sSingleton;
+
+    private final Context mContext;
+
+    private final Map<String, BufferedServiceConnection> mServiceConnections = new HashMap<>();
+
+    private static final int EXECUTOR_IDLE_ALIVE_TIME_SECS = 20;
+    private final Executor mExecutor;
+
+    /**
+     * Creates an {@link ExecutorService} which has no persistent background thread, and ensures
+     * tasks will run in submit order.
+     */
+    private static ExecutorService createExecutorService() {
+        return new ThreadPoolExecutor(
+                /* corePoolSize= */ 0,
+                /* maximumPoolSize= */ 1,
+                EXECUTOR_IDLE_ALIVE_TIME_SECS,
+                TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>() /* Not used */);
+    }
+
+    /**
+     * Sets executor used to construct the singleton.
+     *
+     * <p>If required, this method must be called before calling {@link #getInstance(Context)}.
+     *
+     * <p>If this method is not called, the reporter will run on a newly-created thread.
+     * This newly-created thread will be cleaned up and recreated as necessary when idle.
+     */
+    public static void initialize(@NonNull Context context, @NonNull Executor executor) {
+        if (context == null || executor == null) {
+            throw new NullPointerException();
+        }
+        synchronized (KeyedAppStatesReporter.class) {
+            if (sSingleton != null) {
+                throw new IllegalStateException(
+                        "initialize can only be called once and must be called before "
+                            + "calling getInstance.");
+            }
+            initializeSingleton(context, executor);
+        }
+    }
+
+    /**
+     * Returns an instance of the reporter.
+     *
+     * <p>Creates and initializes an instance if one doesn't already exist.
+     */
+    @NonNull
+    public static KeyedAppStatesReporter getInstance(@NonNull Context context) {
+        if (context == null || context.getApplicationContext() == null) {
+            throw new NullPointerException();
+        }
+        if (sSingleton == null) {
+            synchronized (KeyedAppStatesReporter.class) {
+                if (sSingleton == null) {
+                    initializeSingleton(context, createExecutorService());
+                }
+            }
+        }
+        return sSingleton;
+    }
+
+    private static void initializeSingleton(@NonNull Context context, @NonNull Executor executor) {
+        sSingleton = new SingletonKeyedAppStatesReporter(context, executor);
+        sSingleton.bind();
+    }
+
+    @VisibleForTesting
+    static void resetSingleton() {
+        synchronized (KeyedAppStatesReporter.class) {
+            sSingleton = null;
+        }
+    }
+
+    private SingletonKeyedAppStatesReporter(Context context, Executor executor) {
+        this.mContext = context.getApplicationContext();
+        this.mExecutor = executor;
+    }
+
+    @Override
+    public void setStates(@NonNull Collection<KeyedAppState> states) {
+        setStates(states, false);
+    }
+
+    private void setStates(final Collection<KeyedAppState> states, final boolean immediate) {
+        mExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (states.isEmpty()) {
+                    return;
+                }
+
+                unbindOldBindings();
+                bind();
+
+                send(buildStatesBundle(states), immediate);
+            }
+        });
+    }
+
+    @Override
+    public void setStatesImmediate(@NonNull Collection<KeyedAppState> states) {
+        setStates(states, true);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void bind() {
+        Collection<String> acceptablePackageNames = getDeviceOwnerAndProfileOwnerPackageNames();
+        acceptablePackageNames.add(PHONESKY_PACKAGE_NAME);
+        bind(acceptablePackageNames);
+    }
+
+    private void bind(Collection<String> acceptablePackageNames) {
+        // Remove already-bound packages
+        Collection<String> filteredPackageNames = new HashSet<>();
+        for (String packageName : acceptablePackageNames) {
+            if (!mServiceConnections.containsKey(packageName)) {
+                filteredPackageNames.add(packageName);
+            }
+        }
+
+        if (filteredPackageNames.isEmpty()) {
+            return;
+        }
+
+        Collection<ServiceInfo> serviceInfos =
+                getServiceInfoInPackages(new Intent(ACTION_APP_STATES), filteredPackageNames);
+
+        for (ServiceInfo serviceInfo : serviceInfos) {
+            Intent bindIntent = new Intent();
+            bindIntent.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
+
+            BufferedServiceConnection bufferedServiceConnection =
+                    new BufferedServiceConnection(
+                        mExecutor, mContext, bindIntent, Context.BIND_AUTO_CREATE);
+            bufferedServiceConnection.bindService();
+
+            mServiceConnections.put(serviceInfo.packageName, bufferedServiceConnection);
+        }
+    }
+
+    private Collection<String> getDeviceOwnerAndProfileOwnerPackageNames() {
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+        Collection<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
+
+        if (activeAdmins == null) {
+            return new ArrayList<>();
+        }
+
+        Collection<String> deviceOwnerProfileOwnerPackageNames = new ArrayList<>();
+
+        for (ComponentName componentName : activeAdmins) {
+            if (devicePolicyManager.isDeviceOwnerApp(componentName.getPackageName())
+                    || devicePolicyManager.isProfileOwnerApp(componentName.getPackageName())) {
+                deviceOwnerProfileOwnerPackageNames.add(componentName.getPackageName());
+            }
+        }
+
+        return deviceOwnerProfileOwnerPackageNames;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void unbindOldBindings() {
+        Iterator<Entry<String, BufferedServiceConnection>> iterator =
+                mServiceConnections.entrySet().iterator();
+
+        while (iterator.hasNext()) {
+            Entry<String, BufferedServiceConnection> entry = iterator.next();
+            if (packageNameShouldBeUnbound(entry.getKey())) {
+                entry.getValue().unbind();
+                iterator.remove();
+            }
+        }
+    }
+
+    /** Assumes the given package name is a stored service connection. */
+    private boolean packageNameShouldBeUnbound(String packageName) {
+        if (Build.VERSION.SDK_INT < 26
+                && mServiceConnections.get(packageName).hasBeenDisconnected()) {
+            return true;
+        }
+
+        if (mServiceConnections.get(packageName).isDead()) {
+            return true;
+        }
+
+        if (!canPackageReceiveAppStates(mContext, packageName)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private Collection<ServiceInfo> getServiceInfoInPackages(
+            Intent intent, Collection<String> acceptablePackageNames) {
+        PackageManager packageManager = mContext.getPackageManager();
+        List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(intent, /* flags = */0);
+
+        Collection<ServiceInfo> validServiceInfo = new ArrayList<>();
+        for (ResolveInfo i : resolveInfos) {
+            if (acceptablePackageNames.contains(i.serviceInfo.packageName)) {
+                validServiceInfo.add(i.serviceInfo);
+            }
+        }
+        return validServiceInfo;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static Bundle buildStatesBundle(Collection<KeyedAppState> keyedAppStates) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelableArrayList(APP_STATES, buildStateBundles(keyedAppStates));
+        return bundle;
+    }
+
+    // Returns an ArrayList as required to be used with Bundle#putParcelableArrayList.
+    private static ArrayList<Bundle> buildStateBundles(Collection<KeyedAppState> keyedAppStates) {
+        ArrayList<Bundle> bundles = new ArrayList<>();
+        for (KeyedAppState keyedAppState : keyedAppStates) {
+            bundles.add(keyedAppState.toStateBundle());
+        }
+        return bundles;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void send(Bundle appStatesBundle, boolean immediate) {
+        for (BufferedServiceConnection serviceConnection : mServiceConnections.values()) {
+            // Messages cannot be reused so we create a copy for each service connection.
+            serviceConnection.send(createStateMessage(appStatesBundle, immediate));
+        }
+    }
+
+    private static Message createStateMessage(Bundle appStatesBundle, boolean immediate) {
+        Message message = Message.obtain();
+        message.what = immediate ? WHAT_IMMEDIATE_STATE : WHAT_STATE;
+        message.obj = appStatesBundle;
+        return message;
+    }
+}
diff --git a/enterprise/feedback/src/test/java/androidx/enterprise/feedback/KeyedAppStatesReporterTest.java b/enterprise/feedback/src/test/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporterTest.java
similarity index 94%
rename from enterprise/feedback/src/test/java/androidx/enterprise/feedback/KeyedAppStatesReporterTest.java
rename to enterprise/feedback/src/test/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporterTest.java
index ccce0e4..1b88d1e 100644
--- a/enterprise/feedback/src/test/java/androidx/enterprise/feedback/KeyedAppStatesReporterTest.java
+++ b/enterprise/feedback/src/test/java/androidx/enterprise/feedback/SingletonKeyedAppStatesReporterTest.java
@@ -65,11 +65,11 @@
 import java.util.Collections;
 import java.util.concurrent.Executor;
 
-/** Tests {@link KeyedAppStatesReporter}. */
+/** Tests {@link SingletonKeyedAppStatesReporter}. */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = 21)
-public class KeyedAppStatesReporterTest {
+public class SingletonKeyedAppStatesReporterTest {
 
     private final ComponentName mTestComponentName = new ComponentName("test_package", "");
 
@@ -89,15 +89,15 @@
     @Before
     public void setUp() {
         // Reset the singleton so tests are independent
-        KeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.resetSingleton();
     }
 
     @Test
     @SmallTest
     public void getInstance_nullContext_throwsNullPointerException() {
-        KeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.resetSingleton();
         try {
-            KeyedAppStatesReporter.getInstance(null);
+            SingletonKeyedAppStatesReporter.getInstance(null);
             fail();
         } catch (NullPointerException expected) {
         }
@@ -106,11 +106,11 @@
     @Test
     @SmallTest
     public void initialize_usesExecutor() {
-        KeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.resetSingleton();
         TestExecutor testExecutor = new TestExecutor();
-        KeyedAppStatesReporter.initialize(mContext, testExecutor);
+        SingletonKeyedAppStatesReporter.initialize(mContext, testExecutor);
 
-        KeyedAppStatesReporter.getInstance(mContext).setStates(singleton(mState));
+        SingletonKeyedAppStatesReporter.getInstance(mContext).setStates(singleton(mState));
 
         assertThat(testExecutor.lastExecuted()).isNotNull();
     }
@@ -118,11 +118,11 @@
     @Test
     @SmallTest
     public void initialize_calledMultipleTimes_throwsIllegalStateException() {
-        KeyedAppStatesReporter.resetSingleton();
-        KeyedAppStatesReporter.initialize(mContext, mExecutor);
+        SingletonKeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.initialize(mContext, mExecutor);
 
         try {
-            KeyedAppStatesReporter.initialize(mContext, mExecutor);
+            SingletonKeyedAppStatesReporter.initialize(mContext, mExecutor);
         } catch (IllegalStateException expected) {
         }
     }
@@ -130,11 +130,11 @@
     @Test
     @SmallTest
     public void initialize_calledAfterGetInstance_throwsIllegalStateException() {
-        KeyedAppStatesReporter.resetSingleton();
-        KeyedAppStatesReporter.getInstance(mContext);
+        SingletonKeyedAppStatesReporter.resetSingleton();
+        SingletonKeyedAppStatesReporter.getInstance(mContext);
 
         try {
-            KeyedAppStatesReporter.initialize(mContext, mExecutor);
+            SingletonKeyedAppStatesReporter.initialize(mContext, mExecutor);
         } catch (IllegalStateException expected) {
         }
     }
@@ -500,7 +500,7 @@
     }
 
     private KeyedAppStatesReporter getReporter(Context context) {
-        KeyedAppStatesReporter.initialize(context, mExecutor);
-        return KeyedAppStatesReporter.getInstance(context);
+        SingletonKeyedAppStatesReporter.initialize(context, mExecutor);
+        return SingletonKeyedAppStatesReporter.getInstance(context);
     }
 }
diff --git a/fragment/api/1.1.0-alpha09.txt b/fragment/api/1.1.0-alpha09.txt
index 97f531b..432303b 100644
--- a/fragment/api/1.1.0-alpha09.txt
+++ b/fragment/api/1.1.0-alpha09.txt
@@ -102,6 +102,7 @@
     method @CallSuper public void onPause();
     method public void onPictureInPictureModeChanged(boolean);
     method public void onPrepareOptionsMenu(android.view.Menu);
+    method public void onPrimaryNavigationFragmentChanged(boolean);
     method public void onRequestPermissionsResult(int, String[], int[]);
     method @CallSuper public void onResume();
     method public void onSaveInstanceState(android.os.Bundle);
diff --git a/fragment/api/current.txt b/fragment/api/current.txt
index 97f531b..432303b 100644
--- a/fragment/api/current.txt
+++ b/fragment/api/current.txt
@@ -102,6 +102,7 @@
     method @CallSuper public void onPause();
     method public void onPictureInPictureModeChanged(boolean);
     method public void onPrepareOptionsMenu(android.view.Menu);
+    method public void onPrimaryNavigationFragmentChanged(boolean);
     method public void onRequestPermissionsResult(int, String[], int[]);
     method @CallSuper public void onResume();
     method public void onSaveInstanceState(android.os.Bundle);
diff --git a/fragment/build.gradle b/fragment/build.gradle
index abe4c77..ee79ab7 100644
--- a/fragment/build.gradle
+++ b/fragment/build.gradle
@@ -19,7 +19,7 @@
     api(project(":core")) {
         exclude group: 'androidx.annotation'
     }
-    api(project(":collection"))
+    api("androidx.collection:collection:1.1.0-rc01")
     api("androidx.viewpager:viewpager:1.0.0")
     api("androidx.annotation:annotation:1.1.0-rc01")
     api("androidx.loader:loader:1.0.0")
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
index b1ed36a..25a19c0 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
@@ -19,15 +19,18 @@
 import android.app.Activity
 import androidx.fragment.app.test.EmptyFragmentTestActivity
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
+import androidx.test.filters.MediumTest
 import androidx.test.rule.ActivityTestRule
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
 
 @RunWith(AndroidJUnit4::class)
-@SmallTest
+@MediumTest
 class PrimaryNavFragmentTest {
     @get:Rule
     val activityRule = ActivityTestRule(EmptyFragmentTestActivity::class.java)
@@ -35,7 +38,7 @@
     @Test
     fun delegateBackToPrimaryNav() {
         val fm = activityRule.activity.supportFragmentManager
-        val strictFragment = StrictFragment()
+        val strictFragment = spy(StrictFragment())
 
         fm.beginTransaction()
             .add(strictFragment, null)
@@ -43,6 +46,7 @@
             .commit()
         executePendingTransactions(fm)
 
+        verify(strictFragment).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("new fragment is not primary nav fragment")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment)
@@ -69,7 +73,9 @@
     @Test
     fun popPrimaryNav() {
         val fm = activityRule.activity.supportFragmentManager
-        val strictFragment1 = StrictFragment()
+        val strictFragment1 = spy(StrictFragment())
+        val strictFragment2 = spy(StrictFragment())
+        val inOrder = inOrder(strictFragment1, strictFragment2)
 
         fm.beginTransaction()
             .add(strictFragment1, null)
@@ -77,6 +83,7 @@
             .commit()
         executePendingTransactions(fm)
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("new fragment is not primary nav fragment")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment1)
@@ -87,17 +94,18 @@
             .commit()
         executePendingTransactions(fm)
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(false)
         assertWithMessage("primary nav fragment is not null after remove")
             .that(fm.primaryNavigationFragment)
             .isNull()
 
         activityRule.onBackPressed()
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("primary nav fragment was not restored on pop")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment1)
 
-        val strictFragment2 = StrictFragment()
         fm.beginTransaction()
             .remove(strictFragment1)
             .add(strictFragment2, null)
@@ -106,12 +114,16 @@
             .commit()
         executePendingTransactions(fm)
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(false)
+        inOrder.verify(strictFragment2).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("primary nav fragment not updated to new fragment")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment2)
 
         activityRule.onBackPressed()
 
+        inOrder.verify(strictFragment2).onPrimaryNavigationFragmentChanged(false)
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("primary nav fragment not restored on pop")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment1)
@@ -136,7 +148,9 @@
     @Test
     fun replacePrimaryNav() {
         val fm = activityRule.activity.supportFragmentManager
-        val strictFragment1 = StrictFragment()
+        val strictFragment1 = spy(StrictFragment())
+        val strictFragment2 = spy(StrictFragment())
+        val inOrder = inOrder(strictFragment1, strictFragment2)
 
         fm.beginTransaction()
             .add(android.R.id.content, strictFragment1)
@@ -144,11 +158,11 @@
             .commit()
         executePendingTransactions(fm)
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("new fragment is not primary nav fragment")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment1)
 
-        val strictFragment2 = StrictFragment()
         fm.beginTransaction()
             .replace(android.R.id.content, strictFragment2)
             .addToBackStack(null)
@@ -156,12 +170,14 @@
 
         executePendingTransactions(fm)
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(false)
         assertWithMessage("primary nav fragment not null after replace")
             .that(fm.primaryNavigationFragment)
             .isNull()
 
         activityRule.onBackPressed()
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("primary nav fragment not restored after popping replace")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment1)
@@ -171,6 +187,7 @@
             .commit()
         executePendingTransactions(fm)
 
+        inOrder.verify(strictFragment1).onPrimaryNavigationFragmentChanged(false)
         assertWithMessage("primary nav fragment not null after explicit set to null")
             .that(fm.primaryNavigationFragment)
             .isNull()
@@ -182,12 +199,14 @@
             .commit()
         executePendingTransactions(fm)
 
+        inOrder.verify(strictFragment2).onPrimaryNavigationFragmentChanged(true)
         assertWithMessage("primary nav fragment not set correctly after replace")
             .that(fm.primaryNavigationFragment)
             .isSameAs(strictFragment2)
 
         activityRule.onBackPressed()
 
+        inOrder.verify(strictFragment2).onPrimaryNavigationFragmentChanged(false)
         assertWithMessage("primary nav fragment not null after popping replace")
             .that(fm.primaryNavigationFragment)
             .isNull()
diff --git a/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 1b0313f..df869e7 100644
--- a/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -128,6 +128,9 @@
     // Target request code.
     int mTargetRequestCode;
 
+    // Boolean indicating whether this Fragment is the primary navigation fragment
+    private Boolean mIsPrimaryNavigationFragment = null;
+
     // True if the fragment is in the list of added fragments.
     boolean mAdded;
 
@@ -1814,6 +1817,20 @@
     }
 
     /**
+     * Callback for when the primary navigation state of this Fragment has changed. This can be
+     * the result of the {@link #getFragmentManager() containing FragmentManager} having its
+     * primary navigation fragment changed via
+     * {@link androidx.fragment.app.FragmentTransaction#setPrimaryNavigationFragment} or due to
+     * the primary navigation fragment changing in a parent FragmentManager.
+     *
+     * @param isPrimaryNavigationFragment True if and only if this Fragment and any
+     * {@link #getParentFragment() parent fragment} is set as the primary navigation fragment
+     * via {@link androidx.fragment.app.FragmentTransaction#setPrimaryNavigationFragment}.
+     */
+    public void onPrimaryNavigationFragmentChanged(boolean isPrimaryNavigationFragment) {
+    }
+
+    /**
      * Called when the Fragment is no longer resumed.  This is generally
      * tied to {@link Activity#onPause() Activity.onPause} of the containing
      * Activity's lifecycle.
@@ -2689,6 +2706,19 @@
         }
     }
 
+    void performPrimaryNavigationFragmentChanged() {
+        boolean isPrimaryNavigationFragment = mFragmentManager.isPrimaryNavigation(this);
+        // Only send out the callback / dispatch if the state has changed
+        if (mIsPrimaryNavigationFragment == null
+                || mIsPrimaryNavigationFragment != isPrimaryNavigationFragment) {
+            mIsPrimaryNavigationFragment = isPrimaryNavigationFragment;
+            onPrimaryNavigationFragmentChanged(isPrimaryNavigationFragment);
+            if (mChildFragmentManager != null) {
+                mChildFragmentManager.dispatchPrimaryNavigationFragmentChanged();
+            }
+        }
+    }
+
     void performMultiWindowModeChanged(boolean isInMultiWindowMode) {
         onMultiWindowModeChanged(isInMultiWindowMode);
         if (mChildFragmentManager != null) {
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java b/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java
index f0a8642..7a1532b 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentManagerImpl.java
@@ -205,7 +205,7 @@
      * navigation Fragments to ensure that all of the parent Fragments are the
      * primary navigation Fragment for their associated FragmentManager
      */
-    private boolean isPrimaryNavigation(@Nullable Fragment parent) {
+    boolean isPrimaryNavigation(@Nullable Fragment parent) {
         // If the parent is null, then we're at the root host
         // and we're always the primary navigation
         if (parent == null) {
@@ -2528,7 +2528,7 @@
 
         if (fms.mPrimaryNavActiveWho != null) {
             mPrimaryNav = mActive.get(fms.mPrimaryNavActiveWho);
-            dispatchOnParentPrimaryNavigationFragmentChanged(mPrimaryNav);
+            dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);
         }
         this.mNextFragmentIndex = fms.mNextFragmentIndex;
     }
@@ -2786,21 +2786,21 @@
         }
         Fragment previousPrimaryNav = mPrimaryNav;
         mPrimaryNav = f;
-        dispatchOnParentPrimaryNavigationFragmentChanged(previousPrimaryNav);
-        dispatchOnParentPrimaryNavigationFragmentChanged(mPrimaryNav);
+        dispatchParentPrimaryNavigationFragmentChanged(previousPrimaryNav);
+        dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);
     }
 
-    private void dispatchOnParentPrimaryNavigationFragmentChanged(@Nullable Fragment f) {
-        if (f != null && f.mChildFragmentManager != null) {
-            f.mChildFragmentManager.onParentPrimaryNavigationFragmentChanged();
+    private void dispatchParentPrimaryNavigationFragmentChanged(@Nullable Fragment f) {
+        if (f != null) {
+            f.performPrimaryNavigationFragmentChanged();
         }
     }
 
-    private void onParentPrimaryNavigationFragmentChanged() {
+    void dispatchPrimaryNavigationFragmentChanged() {
         updateOnBackPressedCallbackEnabled();
         // Update all of our child Fragments with the new primary navigation state
         for (Fragment f : mActive.values()) {
-            dispatchOnParentPrimaryNavigationFragmentChanged(f);
+            dispatchParentPrimaryNavigationFragmentChanged(f);
         }
     }
 
diff --git a/graphics/drawable/animated/build.gradle b/graphics/drawable/animated/build.gradle
index 389e65a..2f3439a 100644
--- a/graphics/drawable/animated/build.gradle
+++ b/graphics/drawable/animated/build.gradle
@@ -10,7 +10,7 @@
 dependencies {
     api(project(":vectordrawable"))
     implementation("androidx.interpolator:interpolator:1.0.0")
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/graphics/drawable/static/build.gradle b/graphics/drawable/static/build.gradle
index 5d92ddc..418f40e 100644
--- a/graphics/drawable/static/build.gradle
+++ b/graphics/drawable/static/build.gradle
@@ -10,7 +10,7 @@
 dependencies {
     api("androidx.annotation:annotation:1.1.0-rc01")
     api(project(":core"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/include-composite-deps.gradle b/include-composite-deps.gradle
index c8a795f..2ad98d0 100644
--- a/include-composite-deps.gradle
+++ b/include-composite-deps.gradle
@@ -28,7 +28,6 @@
   File externalRoot = new File(buildScriptDir, '../../external')
 
   includeBuild(new File(externalRoot, 'doclava'))
-  includeBuild(new File(externalRoot, 'jdiff'))
 }
 
 
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index 40c40f1..82b38f6 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -340,10 +340,6 @@
       "to": "androidx/customview/widget/ViewDragHelper{0}"
     },
     {
-      "from": "android/support/v4/widget/DrawerLayout(.*)",
-      "to": "androidx/drawerlayout/widget/DrawerLayout{0}"
-    },
-    {
       "from": "android/support/v4/util/ArrayMap(.*)",
       "to": "androidx/collection/ArrayMap{0}"
     },
@@ -678,6 +674,10 @@
       "to": "ignore"
     },
     {
+      "from": "androidx/drawerlayout/widget/DrawerLayout(.*)",
+      "to": "ignore"
+    },
+    {
       "from": "androidx/transition/(.*)",
       "to": "ignore"
     },
@@ -1120,7 +1120,7 @@
       "to": "androidx/recommendation"
     },
     {
-      "from": "android/support/drawerlayout",
+      "from": "androidx/drawerlayout",
       "to": "androidx/drawerlayout"
     },
     {
@@ -2027,9 +2027,9 @@
     },
     {
       "from": {
-        "groupId": "com.android.support",
+       "groupId": "androidx.drawerlayout",
         "artifactId": "drawerlayout",
-        "version": "{oldSlVersion}"
+        "version": "{newSlVersion}"
       },
       "to": {
         "groupId": "androidx.drawerlayout",
@@ -4241,7 +4241,6 @@
       "android/support/v4/widget/CompoundButtonCompat": "androidx/core/widget/CompoundButtonCompat",
       "android/support/v4/widget/ContentLoadingProgressBar": "androidx/core/widget/ContentLoadingProgressBar",
       "android/support/v4/widget/DirectedAcyclicGraph": "androidx/coordinatorlayout/widget/DirectedAcyclicGraph",
-      "android/support/v4/widget/DrawerLayout": "androidx/drawerlayout/widget/DrawerLayout",
       "android/support/v4/widget/EdgeEffectCompat": "androidx/core/widget/EdgeEffectCompat",
       "android/support/v4/widget/ExploreByTouchHelper": "androidx/customview/widget/ExploreByTouchHelper",
       "android/support/v4/widget/FocusStrategy": "androidx/customview/widget/FocusStrategy",
diff --git a/media2/session/src/androidTest/AndroidManifest.xml b/media2/session/src/androidTest/AndroidManifest.xml
index 874f321..3d49478 100644
--- a/media2/session/src/androidTest/AndroidManifest.xml
+++ b/media2/session/src/androidTest/AndroidManifest.xml
@@ -24,6 +24,7 @@
 
     <application android:usesCleartextTraffic="true">
         <activity android:name="androidx.media2.session.MockActivity" />
+        <activity android:name="androidx.media2.session.SurfaceActivity" />
 
         <!-- Keep the test services synced together with the MockMediaSessionService -->
         <service android:name="androidx.media2.session.MockMediaSessionService">
diff --git a/media2/session/src/androidTest/java/androidx/media2/session/MediaController_SurfaceTest.java b/media2/session/src/androidTest/java/androidx/media2/session/MediaController_SurfaceTest.java
new file mode 100644
index 0000000..043989a
--- /dev/null
+++ b/media2/session/src/androidTest/java/androidx/media2/session/MediaController_SurfaceTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.media2.session;
+
+import static android.content.Context.KEYGUARD_SERVICE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
+import android.os.Build;
+import android.os.Process;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaController#setSurface(Surface)}.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class MediaController_SurfaceTest extends MediaSessionTestBase {
+    private static final String TAG = "MC_SurfaceTest";
+
+    private Instrumentation mInstrumentation;
+    private SurfaceActivity mActivity;
+    private MockPlayer mPlayer;
+    private MediaSession mSession;
+    private MediaController mController;
+
+    @Rule
+    public ActivityTestRule<SurfaceActivity> mActivityRule =
+            new ActivityTestRule<>(SurfaceActivity.class);
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mActivity = mActivityRule.getActivity();
+
+        setKeepScreenOn();
+
+        mPlayer = new MockPlayer(0);
+        mSession = new MediaSession.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, new MediaSession.SessionCallback() {
+                    @Override
+                    public SessionCommandGroup onConnect(
+                            @NonNull MediaSession session,
+                            @NonNull MediaSession.ControllerInfo controller) {
+                        if (Process.myUid() == controller.getUid()) {
+                            return super.onConnect(session, controller);
+                        }
+                        return null;
+                    }
+                })
+                .setId(TAG)
+                .build();
+        mController = createController(mSession.getToken());
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+
+        mSession.close();
+    }
+
+    @Test
+    public void testSetSurface() throws Exception {
+        prepareLooper();
+
+        // Set
+        final Surface testSurface = mActivity.getSurfaceHolder().getSurface();
+        SessionResult result = mController.setSurface(testSurface)
+                .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertEquals(SessionResult.RESULT_SUCCESS, result.getResultCode());
+        assertSame(testSurface, mPlayer.mSurface);
+
+        // Reset
+        result = mController.setSurface(null).get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertEquals(SessionResult.RESULT_SUCCESS, result.getResultCode());
+        assertNull(mPlayer.mSurface);
+    }
+
+    private void setKeepScreenOn() throws Exception {
+        try {
+            setKeepScreenOnOrThrow();
+        } catch (Throwable tr) {
+            throw new Exception(tr);
+        }
+    }
+
+    private void setKeepScreenOnOrThrow() throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (Build.VERSION.SDK_INT >= 27) {
+                    mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                    mActivity.setTurnScreenOn(true);
+                    mActivity.setShowWhenLocked(true);
+                    KeyguardManager keyguardManager = (KeyguardManager)
+                            mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
+                    keyguardManager.requestDismissKeyguard(mActivity, null);
+                } else {
+                    mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                            | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                            | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                            | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+                }
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+    }
+}
diff --git a/media2/session/src/androidTest/java/androidx/media2/session/MockPlayer.java b/media2/session/src/androidTest/java/androidx/media2/session/MockPlayer.java
index a7b9b9e..26b4406 100644
--- a/media2/session/src/androidTest/java/androidx/media2/session/MockPlayer.java
+++ b/media2/session/src/androidTest/java/androidx/media2/session/MockPlayer.java
@@ -16,7 +16,10 @@
 
 package androidx.media2.session;
 
+import android.view.Surface;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.core.util.Pair;
 import androidx.media.AudioAttributesCompat;
 import androidx.media2.common.MediaItem;
@@ -62,6 +65,7 @@
     public @RepeatMode int mRepeatMode = -1;
     public @ShuffleMode int mShuffleMode = -1;
     public VideoSize mVideoSize = new VideoSize(0, 0);
+    public Surface mSurface;
 
     public boolean mSetPlaylistCalled;
     public boolean mUpdatePlaylistMetadataCalled;
@@ -491,4 +495,11 @@
             });
         }
     }
+
+    @Override
+    @NonNull
+    public ListenableFuture<PlayerResult> setSurfaceInternal(@Nullable Surface surface) {
+        mSurface = surface;
+        return new SyncListenableFuture(mCurrentMediaItem);
+    }
 }
diff --git a/media2/session/src/androidTest/java/androidx/media2/session/SurfaceActivity.java b/media2/session/src/androidTest/java/androidx/media2/session/SurfaceActivity.java
new file mode 100644
index 0000000..480d92b
--- /dev/null
+++ b/media2/session/src/androidTest/java/androidx/media2/session/SurfaceActivity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.media2.session;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import androidx.media2.session.test.R;
+
+public class SurfaceActivity extends Activity {
+    private SurfaceHolder mHolder;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_surface);
+
+        SurfaceView surfaceView = findViewById(R.id.surface_view);
+        mHolder = surfaceView.getHolder();
+    }
+
+    public SurfaceHolder getSurfaceHolder() {
+        return mHolder;
+    }
+}
diff --git a/media2/session/src/androidTest/res/layout/activity_surface.xml b/media2/session/src/androidTest/res/layout/activity_surface.xml
new file mode 100644
index 0000000..7945f97
--- /dev/null
+++ b/media2/session/src/androidTest/res/layout/activity_surface.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
+    <SurfaceView
+        android:id="@+id/surface_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    </SurfaceView>
+</LinearLayout>
diff --git a/media2/session/src/main/aidl/androidx/media2/session/IMediaSession.aidl b/media2/session/src/main/aidl/androidx/media2/session/IMediaSession.aidl
index 604ca65..0c55f331 100644
--- a/media2/session/src/main/aidl/androidx/media2/session/IMediaSession.aidl
+++ b/media2/session/src/main/aidl/androidx/media2/session/IMediaSession.aidl
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 import android.net.Uri;
+import android.view.Surface;
 
 import androidx.media2.common.ParcelImplListSlice;
 import androidx.media2.session.IMediaController;
@@ -69,6 +70,7 @@
     void skipToNextItem(IMediaController caller, int seq) = 29;
     void setRepeatMode(IMediaController caller, int seq, int repeatMode) = 30;
     void setShuffleMode(IMediaController caller, int seq, int shuffleMode) = 31;
+    void setSurface(IMediaController caller, int seq, in Surface surface) = 40;
 
     void onControllerResult(IMediaController caller, int seq,
             in ParcelImpl controllerResult) = 32;
@@ -86,5 +88,5 @@
     void subscribe(IMediaController caller, int seq, String parentId,
             in ParcelImpl libraryParams) = 38;
     void unsubscribe(IMediaController caller, int seq, String parentId) = 39;
-    // Next Id : 40
+    // Next Id : 41
 }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaBrowser.java b/media2/session/src/main/java/androidx/media2/session/MediaBrowser.java
index 617b53f..fe4dd9f 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaBrowser.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaBrowser.java
@@ -96,14 +96,11 @@
 
     @Override
     MediaBrowserImpl createImpl(@NonNull Context context, @NonNull SessionToken token,
-            @Nullable Bundle connectionHints, @Nullable Executor executor,
-            @Nullable ControllerCallback callback) {
+            @Nullable Bundle connectionHints) {
         if (token.isLegacySession()) {
-            return new MediaBrowserImplLegacy(
-                    context, this, token, executor, (BrowserCallback) callback);
+            return new MediaBrowserImplLegacy(context, this, token);
         } else {
-            return new MediaBrowserImplBase(context, this, token, connectionHints,
-                    executor, (BrowserCallback) callback);
+            return new MediaBrowserImplBase(context, this, token, connectionHints);
         }
     }
 
@@ -112,11 +109,6 @@
         return (MediaBrowserImpl) super.getImpl();
     }
 
-    @Override
-    BrowserCallback getCallback() {
-        return (BrowserCallback) super.getCallback();
-    }
-
     /**
      * Gets the library root.
      * <p>
@@ -286,6 +278,20 @@
         return createDisconnectedFuture();
     }
 
+    void notifyBrowserCallback(final BrowserCallbackRunnable callbackRunnable) {
+        if (mCallback != null && mCallbackExecutor != null) {
+            mCallbackExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callbackRunnable.run((BrowserCallback) mCallback);
+                }
+            });
+        }
+    }
+
+    interface BrowserCallbackRunnable {
+        void run(@NonNull BrowserCallback callback);
+    }
 
     /**
      * Builder for {@link MediaBrowser}.
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplBase.java b/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplBase.java
index a0dbda4..30affc9 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplBase.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplBase.java
@@ -32,16 +32,16 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.media2.common.MediaParcelUtils;
 import androidx.media2.session.MediaBrowser.BrowserCallback;
+import androidx.media2.session.MediaBrowser.BrowserCallbackRunnable;
 import androidx.media2.session.MediaLibraryService.LibraryParams;
 import androidx.media2.session.SequencedFutureManager.SequencedFuture;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.concurrent.Executor;
-
 /**
  * Base implementation of MediaBrowser.
  */
@@ -51,18 +51,13 @@
             new LibraryResult(RESULT_INFO_SKIPPED);
 
     MediaBrowserImplBase(Context context, MediaController instance, SessionToken token,
-            @Nullable Bundle connectionHints, Executor executor, BrowserCallback callback) {
-        super(context, instance, token, connectionHints, executor, callback);
+            @Nullable Bundle connectionHints) {
+        super(context, instance, token, connectionHints);
     }
 
-    @Override
-    public MediaBrowser.BrowserCallback getCallback() {
-        return (MediaBrowser.BrowserCallback) super.getCallback();
-    }
-
-    @Override
-    public MediaBrowser getInstance() {
-        return (MediaBrowser) super.getInstance();
+    @NonNull
+    MediaBrowser getMediaBrowser() {
+        return (MediaBrowser) mInstance;
     }
 
     @Override
@@ -153,22 +148,20 @@
 
     void notifySearchResultChanged(final String query, final int itemCount,
             final LibraryParams libraryParams) {
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        getMediaBrowser().notifyBrowserCallback(new BrowserCallbackRunnable() {
             @Override
-            public void run() {
-                getCallback().onSearchResultChanged(getInstance(), query, itemCount, libraryParams);
+            public void run(@NonNull BrowserCallback callback) {
+                callback.onSearchResultChanged(getMediaBrowser(), query, itemCount, libraryParams);
             }
         });
     }
 
     void notifyChildrenChanged(final String parentId, final int itemCount,
             final LibraryParams libraryParams) {
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        getMediaBrowser().notifyBrowserCallback(new BrowserCallbackRunnable() {
             @Override
-            public void run() {
-                getCallback().onChildrenChanged(getInstance(), parentId, itemCount, libraryParams);
+            public void run(@NonNull BrowserCallback callback) {
+                callback.onChildrenChanged(getMediaBrowser(), parentId, itemCount, libraryParams);
             }
         });
     }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplLegacy.java b/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplLegacy.java
index 8c3ae11..0b03604 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplLegacy.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaBrowserImplLegacy.java
@@ -39,6 +39,8 @@
 import androidx.concurrent.futures.ResolvableFuture;
 import androidx.media2.common.MediaItem;
 import androidx.media2.common.MediaMetadata;
+import androidx.media2.session.MediaBrowser.BrowserCallback;
+import androidx.media2.session.MediaBrowser.BrowserCallbackRunnable;
 import androidx.media2.session.MediaLibraryService.LibraryParams;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -46,7 +48,6 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
-import java.util.concurrent.Executor;
 
 /**
  * Implementation of MediaBrowser with the {@link MediaBrowserCompat} for legacy support.
@@ -62,14 +63,13 @@
     private final HashMap<String, List<SubscribeCallback>> mSubscribeCallbacks = new HashMap<>();
 
     MediaBrowserImplLegacy(@NonNull Context context, MediaBrowser instance,
-            @NonNull SessionToken token, @Nullable /*@CallbackExecutor*/ Executor executor,
-            @Nullable MediaBrowser.BrowserCallback callback) {
-        super(context, instance, token, executor, callback);
+            @NonNull SessionToken token) {
+        super(context, instance, token);
     }
 
-    @Override
-    public MediaBrowser getInstance() {
-        return (MediaBrowser) super.getInstance();
+    @NonNull
+    MediaBrowser getMediaBrowser() {
+        return (MediaBrowser) mInstance;
     }
 
     @Override
@@ -219,33 +219,31 @@
             @Override
             public void onSearchResult(final String query, final Bundle extras,
                     final List<MediaBrowserCompat.MediaItem> items) {
-                if (mCallback == null) return;
-                mCallbackExecutor.execute(new Runnable() {
+                getMediaBrowser().notifyBrowserCallback(new BrowserCallbackRunnable() {
                     @Override
-                    public void run() {
+                    public void run(@NonNull BrowserCallback callback) {
                         // Set extra null here, because 'extra' have different meanings between old
                         // API and new API as follows.
                         // - Old API: Extra/Option specified with search().
                         // - New API: Extra from MediaLibraryService to MediaBrowser
                         // TODO(Post-P): Cache search result for later getSearchResult() calls.
-                        getCallback().onSearchResultChanged(
-                                getInstance(), query, items.size(), null);
+                        callback.onSearchResultChanged(
+                                getMediaBrowser(), query, items.size(), null);
                     }
                 });
             }
 
             @Override
             public void onError(final String query, final Bundle extras) {
-                if (mCallback == null) return;
-                mCallbackExecutor.execute(new Runnable() {
+                getMediaBrowser().notifyBrowserCallback(new BrowserCallbackRunnable() {
                     @Override
-                    public void run() {
+                    public void run(@NonNull BrowserCallback callback) {
                         // Set extra null here, because 'extra' have different meanings between old
                         // API and new API as follows.
                         // - Old API: Extra/Option specified with search().
                         // - New API: Extra from MediaLibraryService to MediaBrowser
-                        getCallback().onSearchResultChanged(
-                                getInstance(), query, 0, null);
+                        callback.onSearchResultChanged(
+                                getMediaBrowser(), query, 0, null);
                     }
                 });
             }
@@ -293,11 +291,6 @@
         return future;
     }
 
-    @Override
-    public MediaBrowser.BrowserCallback getCallback() {
-        return (MediaBrowser.BrowserCallback) super.getCallback();
-    }
-
     private MediaBrowserCompat getBrowserCompat(LibraryParams extras) {
         synchronized (mLock) {
             return mBrowserCompats.get(extras);
@@ -405,12 +398,11 @@
 
             final LibraryParams params = MediaUtils.convertToLibraryParams(mContext,
                     browserCompat.getNotifyChildrenChangedOptions());
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            getMediaBrowser().notifyBrowserCallback(new BrowserCallbackRunnable() {
                 @Override
-                public void run() {
+                public void run(@NonNull BrowserCallback callback) {
                     // TODO(Post-P): Cache children result for later getChildren() calls.
-                    getCallback().onChildrenChanged(getInstance(), parentId, itemCount, params);
+                    callback.onChildrenChanged(getMediaBrowser(), parentId, itemCount, params);
                 }
             });
         }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaController.java b/media2/session/src/main/java/androidx/media2/session/MediaController.java
index 92a2d3c..327f0a4 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaController.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaController.java
@@ -33,6 +33,7 @@
 import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.session.MediaSessionCompat;
 import android.text.TextUtils;
+import android.view.Surface;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.IntDef;
@@ -126,6 +127,9 @@
     @GuardedBy("mLock")
     boolean mClosed;
 
+    final ControllerCallback mCallback;
+    final Executor mCallbackExecutor;
+
     // For testing.
     Long mTimeDiff;
 
@@ -146,8 +150,10 @@
         if (token == null) {
             throw new NullPointerException("token shouldn't be null");
         }
+        mCallback = callback;
+        mCallbackExecutor = executor;
         synchronized (mLock) {
-            mImpl = createImpl(context, token, connectionHints, executor, callback);
+            mImpl = createImpl(context, token, connectionHints);
         }
     }
 
@@ -168,6 +174,8 @@
         if (token == null) {
             throw new NullPointerException("token shouldn't be null");
         }
+        mCallback = callback;
+        mCallbackExecutor = executor;
         SessionToken.createSessionToken(context, token, executor,
                 new SessionToken.OnSessionTokenCreatedListener() {
                     @Override
@@ -175,12 +183,11 @@
                             SessionToken token2) {
                         synchronized (mLock) {
                             if (!mClosed) {
-                                mImpl = createImpl(context, token2, connectionHints, executor,
-                                        callback);
+                                mImpl = createImpl(context, token2, connectionHints);
                             } else {
-                                executor.execute(new Runnable() {
+                                notifyControllerCallback(new ControllerCallbackRunnable() {
                                     @Override
-                                    public void run() {
+                                    public void run(@NonNull ControllerCallback callback) {
                                         callback.onDisconnected(MediaController.this);
                                     }
                                 });
@@ -191,13 +198,11 @@
     }
 
     MediaControllerImpl createImpl(@NonNull Context context, @NonNull SessionToken token,
-            @Nullable Bundle connectionHints, @Nullable Executor executor,
-            @Nullable ControllerCallback callback) {
+            @Nullable Bundle connectionHints) {
         if (token.isLegacySession()) {
-            return new MediaControllerImplLegacy(context, this, token, executor, callback);
+            return new MediaControllerImplLegacy(context, this, token);
         } else {
-            return new MediaControllerImplBase(context, this, token, connectionHints, executor,
-                    callback);
+            return new MediaControllerImplBase(context, this, token, connectionHints);
         }
     }
 
@@ -1086,6 +1091,24 @@
     }
 
     /**
+     * Sets the {@link Surface} to be used as the sink for the video portion of the media.
+     * <p>
+     * This calls {@link SessionPlayer#setSurfaceInternal(Surface)}.
+     *
+     * @param surface The {@link Surface} to be used for the video portion of the media.
+     * @return a {@link ListenableFuture} which represents the pending completion of the command.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public ListenableFuture<SessionResult> setSurface(@Nullable Surface surface) {
+        if (isConnected()) {
+            return getImpl().setSurface(surface);
+        }
+        return createDisconnectedFuture();
+    }
+
+    /**
      * Sets the time diff forcefully when calculating current position.
      * @param timeDiff {@code null} for reset.
      *
@@ -1101,14 +1124,19 @@
                 SessionResult.RESULT_ERROR_SESSION_DISCONNECTED);
     }
 
-    @Nullable
-    ControllerCallback getCallback() {
-        return isConnected() ? getImpl().getCallback() : null;
+    void notifyControllerCallback(final ControllerCallbackRunnable callbackRunnable) {
+        if (mCallback != null && mCallbackExecutor != null) {
+            mCallbackExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    callbackRunnable.run(mCallback);
+                }
+            });
+        }
     }
 
-    @Nullable
-    Executor getCallbackExecutor() {
-        return isConnected() ? getImpl().getCallbackExecutor() : null;
+    interface ControllerCallbackRunnable {
+        void run(@NonNull ControllerCallback callback);
     }
 
     interface MediaControllerImpl extends AutoCloseable {
@@ -1172,12 +1200,10 @@
         @ShuffleMode int getShuffleMode();
         ListenableFuture<SessionResult> setShuffleMode(@ShuffleMode int shuffleMode);
         @NonNull VideoSize getVideoSize();
+        ListenableFuture<SessionResult> setSurface(@Nullable Surface surface);
 
         // Internally used methods
-        @NonNull MediaController getInstance();
         @NonNull Context getContext();
-        @Nullable ControllerCallback getCallback();
-        @Nullable Executor getCallbackExecutor();
         @Nullable MediaBrowserCompat getBrowserCompat();
     }
 
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaControllerImplBase.java b/media2/session/src/main/java/androidx/media2/session/MediaControllerImplBase.java
index e81e24bc..cd6905d 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaControllerImplBase.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaControllerImplBase.java
@@ -32,6 +32,7 @@
 import static androidx.media2.session.SessionCommand.COMMAND_CODE_PLAYER_SET_REPEAT_MODE;
 import static androidx.media2.session.SessionCommand.COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE;
 import static androidx.media2.session.SessionCommand.COMMAND_CODE_PLAYER_SET_SPEED;
+import static androidx.media2.session.SessionCommand.COMMAND_CODE_PLAYER_SET_SURFACE;
 import static androidx.media2.session.SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM;
 import static androidx.media2.session.SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM;
 import static androidx.media2.session.SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM;
@@ -68,6 +69,7 @@
 import android.os.SystemClock;
 import android.support.v4.media.MediaBrowserCompat;
 import android.util.Log;
+import android.view.Surface;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -81,6 +83,7 @@
 import androidx.media2.common.SessionPlayer.ShuffleMode;
 import androidx.media2.common.VideoSize;
 import androidx.media2.session.MediaController.ControllerCallback;
+import androidx.media2.session.MediaController.ControllerCallbackRunnable;
 import androidx.media2.session.MediaController.MediaControllerImpl;
 import androidx.media2.session.MediaController.PlaybackInfo;
 import androidx.media2.session.MediaController.VolumeDirection;
@@ -89,7 +92,6 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.List;
-import java.util.concurrent.Executor;
 
 class MediaControllerImplBase implements MediaControllerImpl {
     private static final boolean THROW_EXCEPTION_FOR_NULL_RESULT = true;
@@ -106,10 +108,6 @@
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final SessionToken mToken;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final ControllerCallback mCallback;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final Executor mCallbackExecutor;
     private final IBinder.DeathRecipient mDeathRecipient;
     final SequencedFutureManager mSequencedFutureManager;
     final MediaControllerStub mControllerStub;
@@ -163,7 +161,7 @@
     private volatile IMediaSession mISession;
 
     MediaControllerImplBase(Context context, MediaController instance, SessionToken token,
-            @Nullable Bundle connectionHints, Executor executor, ControllerCallback callback) {
+            @Nullable Bundle connectionHints) {
         mInstance = instance;
         if (context == null) {
             throw new NullPointerException("context shouldn't be null");
@@ -175,8 +173,6 @@
         mSequencedFutureManager = new SequencedFutureManager();
         mControllerStub = new MediaControllerStub(this, mSequencedFutureManager);
         mToken = token;
-        mCallback = callback;
-        mCallbackExecutor = executor;
         mDeathRecipient = new IBinder.DeathRecipient() {
             @Override
             public void binderDied() {
@@ -228,11 +224,10 @@
             }
         }
         mSequencedFutureManager.close();
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
-                mCallback.onDisconnected(mInstance);
+            public void run(@NonNull ControllerCallback callback) {
+                callback.onDisconnected(mInstance);
             }
         });
     }
@@ -781,6 +776,17 @@
     }
 
     @Override
+    public ListenableFuture<SessionResult> setSurface(final @Nullable Surface surface) {
+        return dispatchRemoteSessionTask(COMMAND_CODE_PLAYER_SET_SURFACE,
+                new RemoteSessionTask() {
+                    @Override
+                    public void run(IMediaSession iSession, int seq) throws RemoteException {
+                        iSession.setSurface(mControllerStub, seq, surface);
+                    }
+                });
+    }
+
+    @Override
     @NonNull
     public Context getContext() {
         return mContext;
@@ -788,28 +794,10 @@
 
     @Override
     @Nullable
-    public ControllerCallback getCallback() {
-        return mCallback;
-    }
-
-    @Override
-    @Nullable
-    public Executor getCallbackExecutor() {
-        return mCallbackExecutor;
-    }
-
-    @Override
-    @Nullable
     public MediaBrowserCompat getBrowserCompat() {
         return null;
     }
 
-    @Override
-    @NonNull
-    public MediaController getInstance() {
-        return mInstance;
-    }
-
     private boolean requestConnectToService() {
         // Service. Needs to get fresh binder whenever connection is needed.
         final Intent intent = new Intent(MediaSessionService.SERVICE_INTERFACE);
@@ -893,14 +881,13 @@
                 mPlaylist.set(currentMediaItemIndex, item);
             }
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onCurrentMediaItemChanged(mInstance, item);
+                callback.onCurrentMediaItemChanged(mInstance, item);
             }
         });
     }
@@ -911,14 +898,13 @@
             mPositionMs = positionMs;
             mPlayerState = state;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onPlayerStateChanged(mInstance, state);
+                callback.onPlayerStateChanged(mInstance, state);
             }
         });
     }
@@ -929,14 +915,13 @@
             mPositionMs = positionMs;
             mPlaybackSpeed = speed;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onPlaybackSpeedChanged(mInstance, speed);
+                callback.onPlaybackSpeedChanged(mInstance, speed);
             }
         });
     }
@@ -949,14 +934,13 @@
             mPositionEventTimeMs = eventTimeMs;
             mPositionMs = positionMs;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onBufferingStateChanged(mInstance, item, state);
+                callback.onBufferingStateChanged(mInstance, item, state);
             }
         });
     }
@@ -973,14 +957,13 @@
                 mCurrentMediaItem = playlist.get(currentMediaItemIndex);
             }
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onPlaylistChanged(mInstance, playlist, metadata);
+                callback.onPlaylistChanged(mInstance, playlist, metadata);
             }
         });
     }
@@ -989,14 +972,13 @@
         synchronized (mLock) {
             mPlaylistMetadata = metadata;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onPlaylistMetadataChanged(mInstance, metadata);
+                callback.onPlaylistMetadataChanged(mInstance, metadata);
             }
         });
     }
@@ -1005,14 +987,13 @@
         synchronized (mLock) {
             mPlaybackInfo = info;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onPlaybackInfoChanged(mInstance, info);
+                callback.onPlaybackInfoChanged(mInstance, info);
             }
         });
     }
@@ -1025,14 +1006,13 @@
             mPreviousMediaItemIndex = previousMediaItemIndex;
             mNextMediaItemIndex = nextMediaItemIndex;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onRepeatModeChanged(mInstance, repeatMode);
+                callback.onRepeatModeChanged(mInstance, repeatMode);
             }
         });
     }
@@ -1045,27 +1025,25 @@
             mPreviousMediaItemIndex = previousMediaItemIndex;
             mNextMediaItemIndex = nextMediaItemIndex;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onShuffleModeChanged(mInstance, shuffleMode);
+                callback.onShuffleModeChanged(mInstance, shuffleMode);
             }
         });
     }
 
     void notifyPlaybackCompleted() {
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onPlaybackCompleted(mInstance);
+                callback.onPlaybackCompleted(mInstance);
             }
         });
     }
@@ -1076,14 +1054,13 @@
             mPositionMs = positionMs;
         }
 
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onSeekCompleted(mInstance, seekPositionMs);
+                callback.onSeekCompleted(mInstance, seekPositionMs);
             }
         });
     }
@@ -1092,14 +1069,13 @@
         synchronized (mLock) {
             mVideoSize = videoSize;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
+            public void run(@NonNull ControllerCallback callback) {
                 if (!mInstance.isConnected()) {
                     return;
                 }
-                mCallback.onVideoSizeChanged(mInstance, item, videoSize);
+                callback.onVideoSizeChanged(mInstance, item, videoSize);
             }
         });
     }
@@ -1177,11 +1153,10 @@
                         mToken.getUid(), TYPE_SESSION, mToken.getPackageName(), sessionBinder,
                         tokenExtras));
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onConnected(mInstance, allowedCommands);
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onConnected(mInstance, allowedCommands);
                 }
             });
         } finally {
@@ -1214,11 +1189,10 @@
         if (DEBUG) {
             Log.d(TAG, "onCustomCommand cmd=" + command.getCustomAction());
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
-                SessionResult result = mCallback.onCustomCommand(mInstance, command, args);
+            public void run(@NonNull ControllerCallback callback) {
+                SessionResult result = callback.onCustomCommand(mInstance, command, args);
                 if (result == null) {
                     if (THROW_EXCEPTION_FOR_NULL_RESULT) {
                         throw new RuntimeException("ControllerCallback#onCustomCommand() has"
@@ -1233,21 +1207,19 @@
     }
 
     void onAllowedCommandsChanged(final SessionCommandGroup commands) {
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
-                mCallback.onAllowedCommandsChanged(mInstance, commands);
+            public void run(@NonNull ControllerCallback callback) {
+                callback.onAllowedCommandsChanged(mInstance, commands);
             }
         });
     }
 
     void onSetCustomLayout(final int seq, final List<MediaSession.CommandButton> layout) {
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
-                int resultCode = mCallback.onSetCustomLayout(mInstance, layout);
+            public void run(@NonNull ControllerCallback callback) {
+                int resultCode = callback.onSetCustomLayout(mInstance, layout);
                 SessionResult result = new SessionResult(resultCode);
                 sendControllerResult(seq, result);
             }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java b/media2/session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
index 50eef62..d85d75b 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
@@ -52,6 +52,7 @@
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.text.TextUtils;
 import android.util.Log;
+import android.view.Surface;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -68,6 +69,7 @@
 import androidx.media2.common.SessionPlayer.ShuffleMode;
 import androidx.media2.common.VideoSize;
 import androidx.media2.session.MediaController.ControllerCallback;
+import androidx.media2.session.MediaController.ControllerCallbackRunnable;
 import androidx.media2.session.MediaController.PlaybackInfo;
 import androidx.media2.session.MediaController.VolumeDirection;
 import androidx.media2.session.MediaController.VolumeFlags;
@@ -77,7 +79,6 @@
 
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.Executor;
 
 // TODO: Find better way to return listenable future.
 @SuppressLint("ObsoleteSdkInt") // TODO: Remove once the minSdkVersion is lowered enough.
@@ -103,10 +104,6 @@
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final SessionToken mToken;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final ControllerCallback mCallback;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final Executor mCallbackExecutor;
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final HandlerThread mHandlerThread;
@@ -184,16 +181,13 @@
     private volatile boolean mConnected;
 
     MediaControllerImplLegacy(@NonNull Context context, @NonNull MediaController instance,
-            @NonNull SessionToken token, @Nullable Executor executor,
-            @Nullable ControllerCallback callback) {
+            @NonNull SessionToken token) {
         mContext = context;
         mInstance = instance;
         mHandlerThread = new HandlerThread("MediaController_Thread");
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mToken = token;
-        mCallback = callback;
-        mCallbackExecutor = executor;
 
         if (mToken.getType() == SessionToken.TYPE_SESSION) {
             synchronized (mLock) {
@@ -235,11 +229,10 @@
             }
             mConnected = false;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
-                mCallback.onDisconnected(mInstance);
+            public void run(@NonNull ControllerCallback callback) {
+                callback.onDisconnected(mInstance);
             }
         });
     }
@@ -810,32 +803,23 @@
     }
 
     @Override
+    public ListenableFuture<SessionResult> setSurface(@Nullable Surface surface) {
+        Log.w(TAG, "Session doesn't support setting Surface");
+        return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
+    }
+
+    @Override
     public @NonNull Context getContext() {
         return mContext;
     }
 
     @Override
-    public @NonNull ControllerCallback getCallback() {
-        return mCallback;
-    }
-
-    @Override
-    public @NonNull Executor getCallbackExecutor() {
-        return mCallbackExecutor;
-    }
-
-    @Override
     public @Nullable MediaBrowserCompat getBrowserCompat() {
         synchronized (mLock) {
             return mBrowserCompat;
         }
     }
 
-    @Override
-    public @NonNull MediaController getInstance() {
-        return mInstance;
-    }
-
     // Should be used without a lock to prevent potential deadlock.
     void onConnectedNotLocked() {
         if (DEBUG) {
@@ -880,18 +864,17 @@
             setCurrentMediaItemLocked(mControllerCompat.getMetadata());
             mConnected = true;
         }
-        if (mCallback == null) return;
-        mCallbackExecutor.execute(new Runnable() {
+        mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
             @Override
-            public void run() {
-                mCallback.onConnected(mInstance, allowedCommands);
+            public void run(@NonNull ControllerCallback callback) {
+                callback.onConnected(mInstance, allowedCommands);
             }
         });
         if (!customLayout.isEmpty()) {
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onSetCustomLayout(mInstance, customLayout);
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onSetCustomLayout(mInstance, customLayout);
                 }
             });
         }
@@ -1073,12 +1056,11 @@
                     return;
                 }
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
+                public void run(@NonNull ControllerCallback callback) {
                     // Ignore return because legacy session cannot get result back.
-                    mCallback.onCustomCommand(mInstance, new SessionCommand(event, null), extras);
+                    callback.onCustomCommand(mInstance, new SessionCommand(event, null), extras);
                 }
             });
         }
@@ -1123,41 +1105,41 @@
                 currentAllowedCommands = mAllowedCommands;
             }
 
-            if (mCallback == null) return;
+            if (mInstance.mCallback == null) return;
             if (prevItem != currentItem) {
-                mCallbackExecutor.execute(new Runnable() {
+                mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                     @Override
-                    public void run() {
-                        mCallback.onCurrentMediaItemChanged(mInstance, currentItem);
+                    public void run(@NonNull ControllerCallback callback) {
+                        callback.onCurrentMediaItemChanged(mInstance, currentItem);
                     }
                 });
             }
 
             if (state == null) {
                 if (prevState != null) {
-                    mCallbackExecutor.execute(new Runnable() {
+                    mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                         @Override
-                        public void run() {
-                            mCallback.onPlayerStateChanged(mInstance, PLAYER_STATE_IDLE);
+                        public void run(@NonNull ControllerCallback callback) {
+                            callback.onPlayerStateChanged(mInstance, PLAYER_STATE_IDLE);
                         }
                     });
                 }
                 return;
             }
             if (prevState == null || prevState.getState() != state.getState()) {
-                mCallbackExecutor.execute(new Runnable() {
+                mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                     @Override
-                    public void run() {
-                        mCallback.onPlayerStateChanged(
+                    public void run(@NonNull ControllerCallback callback) {
+                        callback.onPlayerStateChanged(
                                 mInstance, MediaUtils.convertToPlayerState(state));
                     }
                 });
             }
             if (prevState == null || prevState.getPlaybackSpeed() != state.getPlaybackSpeed()) {
-                mCallbackExecutor.execute(new Runnable() {
+                mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                     @Override
-                    public void run() {
-                        mCallback.onPlaybackSpeedChanged(mInstance, state.getPlaybackSpeed());
+                    public void run(@NonNull ControllerCallback callback) {
+                        callback.onPlaybackSpeedChanged(mInstance, state.getPlaybackSpeed());
                     }
                 });
             }
@@ -1167,10 +1149,10 @@
                 long positionDiff = Math.abs(currentPosition
                         - prevState.getCurrentPosition(mInstance.mTimeDiff));
                 if (positionDiff > POSITION_DIFF_TOLERANCE) {
-                    mCallbackExecutor.execute(new Runnable() {
+                    mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                         @Override
-                        public void run() {
-                            mCallback.onSeekCompleted(mInstance, currentPosition);
+                        public void run(@NonNull ControllerCallback callback) {
+                            callback.onSeekCompleted(mInstance, currentPosition);
                         }
                     });
                 }
@@ -1180,10 +1162,10 @@
             Set<SessionCommand> currentCommands = currentAllowedCommands.getCommands();
             if (prevCommands.size() != currentCommands.size()
                     || !prevCommands.containsAll(currentCommands)) {
-                mCallbackExecutor.execute(new Runnable() {
+                mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                     @Override
-                    public void run() {
-                        mCallback.onAllowedCommandsChanged(mInstance, currentAllowedCommands);
+                    public void run(@NonNull ControllerCallback callback) {
+                        callback.onAllowedCommandsChanged(mInstance, currentAllowedCommands);
                     }
                 });
             }
@@ -1201,10 +1183,10 @@
                 layoutChanged = true;
             }
             if (layoutChanged) {
-                mCallbackExecutor.execute(new Runnable() {
+                mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                     @Override
-                    public void run() {
-                        mCallback.onSetCustomLayout(mInstance, currentLayout);
+                    public void run(@NonNull ControllerCallback callback) {
+                        callback.onSetCustomLayout(mInstance, currentLayout);
                     }
                 });
             }
@@ -1218,10 +1200,10 @@
                     ? SessionPlayer.BUFFERING_STATE_UNKNOWN
                     : MediaUtils.toBufferingState(prevState.getState());
             if (bufferingState != prevBufferingState) {
-                mCallbackExecutor.execute(new Runnable() {
+                mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                     @Override
-                    public void run() {
-                        mCallback.onBufferingStateChanged(mInstance, currentItem, bufferingState);
+                    public void run(@NonNull ControllerCallback callback) {
+                        callback.onBufferingStateChanged(mInstance, currentItem, bufferingState);
                     }
                 });
             }
@@ -1239,12 +1221,11 @@
                 setCurrentMediaItemLocked(metadata);
                 currentItem = mCurrentMediaItem;
             }
-            if (mCallback == null) return;
             if (prevItem != currentItem) {
-                mCallbackExecutor.execute(new Runnable() {
+                mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                     @Override
-                    public void run() {
-                        mCallback.onCurrentMediaItemChanged(mInstance, currentItem);
+                    public void run(@NonNull ControllerCallback callback) {
+                        callback.onCurrentMediaItemChanged(mInstance, currentItem);
                     }
                 });
             }
@@ -1271,11 +1252,10 @@
                 playlist = mPlaylist;
                 playlistMetadata = mPlaylistMetadata;
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onPlaylistChanged(mInstance, playlist, playlistMetadata);
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onPlaylistChanged(mInstance, playlist, playlistMetadata);
                 }
             });
         }
@@ -1290,11 +1270,10 @@
                 mPlaylistMetadata = MediaUtils.convertToMediaMetadata(title);
                 playlistMetadata = mPlaylistMetadata;
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onPlaylistMetadataChanged(mInstance, playlistMetadata);
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onPlaylistMetadataChanged(mInstance, playlistMetadata);
                 }
             });
         }
@@ -1306,11 +1285,10 @@
                     return;
                 }
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onCustomCommand(mInstance, new SessionCommand(
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onCustomCommand(mInstance, new SessionCommand(
                             SESSION_COMMAND_ON_EXTRAS_CHANGED, null), extras);
                 }
             });
@@ -1323,11 +1301,10 @@
                     return;
                 }
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onPlaybackInfoChanged(mInstance, MediaUtils.toPlaybackInfo2(info));
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onPlaybackInfoChanged(mInstance, MediaUtils.toPlaybackInfo2(info));
                 }
             });
         }
@@ -1339,13 +1316,12 @@
                     return;
                 }
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
+                public void run(@NonNull ControllerCallback callback) {
                     Bundle args = new Bundle();
                     args.putBoolean(MediaConstants.ARGUMENT_CAPTIONING_ENABLED, enabled);
-                    mCallback.onCustomCommand(mInstance, new SessionCommand(
+                    callback.onCustomCommand(mInstance, new SessionCommand(
                             SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED, null), args);
                 }
             });
@@ -1359,11 +1335,10 @@
                 }
                 mRepeatMode = repeatMode;
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onRepeatModeChanged(mInstance, repeatMode);
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onRepeatModeChanged(mInstance, repeatMode);
                 }
             });
         }
@@ -1376,11 +1351,10 @@
                 }
                 mShuffleMode = shuffleMode;
             }
-            if (mCallback == null) return;
-            mCallbackExecutor.execute(new Runnable() {
+            mInstance.notifyControllerCallback(new ControllerCallbackRunnable() {
                 @Override
-                public void run() {
-                    mCallback.onShuffleModeChanged(mInstance, shuffleMode);
+                public void run(@NonNull ControllerCallback callback) {
+                    callback.onShuffleModeChanged(mInstance, shuffleMode);
                 }
             });
         }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaControllerStub.java b/media2/session/src/main/java/androidx/media2/session/MediaControllerStub.java
index f9bab74..73271b7 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaControllerStub.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaControllerStub.java
@@ -305,7 +305,7 @@
                 }
                 return;
             }
-            controller.getInstance().close();
+            controller.mInstance.close();
         } finally {
             Binder.restoreCallingIdentity(token);
         }
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaInterface.java b/media2/session/src/main/java/androidx/media2/session/MediaInterface.java
index 567f6d4c..24898b3 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaInterface.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaInterface.java
@@ -16,6 +16,8 @@
 
 package androidx.media2.session;
 
+import android.view.Surface;
+
 import androidx.media2.common.MediaItem;
 import androidx.media2.common.MediaMetadata;
 import androidx.media2.common.SessionPlayer.PlayerResult;
@@ -76,5 +78,6 @@
     // Common interface for session and controller
     interface SessionPlayer extends SessionPlaybackControl, SessionPlaylistControl {
         VideoSize getVideoSize();
+        ListenableFuture<PlayerResult> setSurface(Surface surface);
     }
 }
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 0aea8b8..e7af683 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -54,6 +54,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.view.Surface;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -850,6 +851,16 @@
         }, new VideoSize(0, 0));
     }
 
+    @Override
+    public ListenableFuture<PlayerResult> setSurface(final Surface surface) {
+        return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
+            @Override
+            public ListenableFuture<PlayerResult> run(@NonNull SessionPlayer player) {
+                return player.setSurfaceInternal(surface);
+            }
+        });
+    }
+
     ///////////////////////////////////////////////////
     // package private and private methods
     ///////////////////////////////////////////////////
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSessionLegacyStub.java b/media2/session/src/main/java/androidx/media2/session/MediaSessionLegacyStub.java
index 2f59751..ebcfbd3 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionLegacyStub.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionLegacyStub.java
@@ -19,6 +19,7 @@
 import static androidx.media2.common.MediaMetadata.METADATA_KEY_DISPLAY_TITLE;
 import static androidx.media2.common.MediaMetadata.METADATA_KEY_TITLE;
 import static androidx.media2.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES;
+import static androidx.media2.session.SessionCommand.COMMAND_VERSION_CURRENT;
 import static androidx.media2.session.SessionResult.RESULT_SUCCESS;
 
 import android.content.Context;
@@ -67,8 +68,8 @@
 
     static {
         SessionCommandGroup group = new SessionCommandGroup.Builder()
-                .addAllPlayerCommands(SessionCommand.COMMAND_VERSION_CURRENT)
-                .addAllVolumeCommands(SessionCommand.COMMAND_VERSION_CURRENT)
+                .addAllPlayerCommands(COMMAND_VERSION_CURRENT, /* includeHidden= */ false)
+                .addAllVolumeCommands(COMMAND_VERSION_CURRENT)
                 .build();
         Set<SessionCommand> commands = group.getCommands();
         for (SessionCommand command : commands) {
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSessionStub.java b/media2/session/src/main/java/androidx/media2/session/MediaSessionStub.java
index ac0fd5f..f45d484 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionStub.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionStub.java
@@ -33,6 +33,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
+import android.view.Surface;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -79,7 +80,7 @@
 
     static {
         SessionCommandGroup group = new SessionCommandGroup.Builder()
-                .addAllPlayerCommands(COMMAND_VERSION_CURRENT)
+                .addAllPlayerCommands(COMMAND_VERSION_CURRENT, /* includeHidden= */ false)
                 .addAllVolumeCommands(COMMAND_VERSION_CURRENT)
                 .build();
         Set<SessionCommand> commands = group.getCommands();
@@ -1018,6 +1019,20 @@
                 });
     }
 
+    @Override
+    public void setSurface(IMediaController caller, int seq, final Surface surface) {
+        if (caller == null) {
+            return;
+        }
+        dispatchSessionTask(caller, seq, SessionCommand.COMMAND_CODE_PLAYER_SET_SURFACE,
+                new SessionPlayerTask() {
+                    @Override
+                    public ListenableFuture<PlayerResult> run(ControllerInfo controller) {
+                        return mSessionImpl.setSurface(surface);
+                    }
+                });
+    }
+
     //////////////////////////////////////////////////////////////////////////////////////////////
     // AIDL methods for LibrarySession overrides
     //////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaUtils.java b/media2/session/src/main/java/androidx/media2/session/MediaUtils.java
index ec6ce2d..c1210ab 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaUtils.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaUtils.java
@@ -804,11 +804,10 @@
     public static SessionCommandGroup convertToSessionCommandGroup(long sessionFlags,
             PlaybackStateCompat state) {
         SessionCommandGroup.Builder commandsBuilder = new SessionCommandGroup.Builder();
+        commandsBuilder.addAllPlayerBasicCommands(COMMAND_VERSION_CURRENT);
         boolean includePlaylistCommands = (sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0;
         if (includePlaylistCommands) {
-            commandsBuilder.addAllPlayerCommands(COMMAND_VERSION_CURRENT);
-        } else {
-            commandsBuilder.addAllPlayerBasicCommands(COMMAND_VERSION_CURRENT);
+            commandsBuilder.addAllPlayerPlaylistCommands(COMMAND_VERSION_CURRENT);
         }
         commandsBuilder.addAllVolumeCommands(COMMAND_VERSION_CURRENT);
         commandsBuilder.addAllSessionCommands(COMMAND_VERSION_CURRENT);
diff --git a/media2/session/src/main/java/androidx/media2/session/SessionCommand.java b/media2/session/src/main/java/androidx/media2/session/SessionCommand.java
index 0a892b0..acbfeba 100644
--- a/media2/session/src/main/java/androidx/media2/session/SessionCommand.java
+++ b/media2/session/src/main/java/androidx/media2/session/SessionCommand.java
@@ -16,11 +16,13 @@
 
 package androidx.media2.session;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
 import android.net.Uri;
 import android.os.Bundle;
 import android.text.TextUtils;
+import android.view.Surface;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
@@ -99,6 +101,7 @@
             COMMAND_CODE_PLAYER_GET_CURRENT_MEDIA_ITEM,
             COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA,
             COMMAND_CODE_PLAYER_SET_MEDIA_ITEM,
+            COMMAND_CODE_PLAYER_SET_SURFACE,
             COMMAND_CODE_VOLUME_SET_VOLUME,
             COMMAND_CODE_VOLUME_ADJUST_VOLUME,
             COMMAND_CODE_SESSION_FAST_FORWARD,
@@ -134,6 +137,7 @@
     ////////////////////////////////////////////////////////////////////////////////////////////////
     static final ArrayMap<Integer, Range> VERSION_PLAYER_BASIC_COMMANDS_MAP = new ArrayMap<>();
     static final ArrayMap<Integer, Range> VERSION_PLAYER_PLAYLIST_COMMANDS_MAP = new ArrayMap<>();
+    static final ArrayMap<Integer, Range> VERSION_PLAYER_HIDDEN_COMMANDS_MAP = new ArrayMap<>();
 
     /**
      * Command code for {@link MediaController#play()}.
@@ -336,6 +340,20 @@
      */
     public static final int COMMAND_CODE_PLAYER_SET_MEDIA_ITEM = 10018;
 
+    /**
+     * Command code for {@link MediaController#setSurface(Surface)}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the
+     * {@link SessionCallback#onCommandRequest(MediaSession, ControllerInfo, SessionCommand)}.
+     * <p>
+     * Code version is {@link #COMMAND_VERSION_1}.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final int COMMAND_CODE_PLAYER_SET_SURFACE = 11000;
+
     static {
         VERSION_PLAYER_BASIC_COMMANDS_MAP.put(COMMAND_VERSION_1,
                 new Range(COMMAND_CODE_PLAYER_PLAY, COMMAND_CODE_PLAYER_SET_SPEED));
@@ -347,6 +365,11 @@
                         COMMAND_CODE_PLAYER_SET_MEDIA_ITEM));
     }
 
+    static {
+        VERSION_PLAYER_HIDDEN_COMMANDS_MAP.put(COMMAND_VERSION_1,
+                new Range(COMMAND_CODE_PLAYER_SET_SURFACE, COMMAND_CODE_PLAYER_SET_SURFACE));
+    }
+
     ////////////////////////////////////////////////////////////////////////////////////////////////
     // Volume commands (i.e. commands to {@link AudioManager} or {@link RouteMediaPlayer})
     ////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/media2/session/src/main/java/androidx/media2/session/SessionCommandGroup.java b/media2/session/src/main/java/androidx/media2/session/SessionCommandGroup.java
index 90cdf8d..ddc20f7 100644
--- a/media2/session/src/main/java/androidx/media2/session/SessionCommandGroup.java
+++ b/media2/session/src/main/java/androidx/media2/session/SessionCommandGroup.java
@@ -148,7 +148,7 @@
             if (version != COMMAND_VERSION_1) {
                 throw new IllegalArgumentException("Unknown command version " + version);
             }
-            addAllPlayerCommands(version);
+            addAllPlayerCommands(version, /* includeHidden= */ true);
             addAllVolumeCommands(version);
             addAllSessionCommands(version);
             addAllLibraryCommands(version);
@@ -168,9 +168,10 @@
             return this;
         }
 
-        @NonNull Builder addAllPlayerCommands(@CommandVersion int version) {
+        @NonNull Builder addAllPlayerCommands(@CommandVersion int version, boolean includeHidden) {
             addAllPlayerBasicCommands(version);
             addAllPlayerPlaylistCommands(version);
+            if (includeHidden) addAllPlayerHiddenCommands(version);
             return this;
         }
 
@@ -184,6 +185,11 @@
             return this;
         }
 
+        @NonNull Builder addAllPlayerHiddenCommands(@CommandVersion int version) {
+            addCommands(version, SessionCommand.VERSION_PLAYER_HIDDEN_COMMANDS_MAP);
+            return this;
+        }
+
         @NonNull Builder addAllVolumeCommands(@CommandVersion int version) {
             addCommands(version, SessionCommand.VERSION_VOLUME_COMMANDS_MAP);
             return this;
diff --git a/media2/session/version-compat-tests/common/src/main/aidl/androidx/media2/test/common/IRemoteMediaSession.aidl b/media2/session/version-compat-tests/common/src/main/aidl/androidx/media2/test/common/IRemoteMediaSession.aidl
index 4f57d6b..efd7850 100644
--- a/media2/session/version-compat-tests/common/src/main/aidl/androidx/media2/test/common/IRemoteMediaSession.aidl
+++ b/media2/session/version-compat-tests/common/src/main/aidl/androidx/media2/test/common/IRemoteMediaSession.aidl
@@ -49,6 +49,7 @@
     void notifyCurrentMediaItemChanged(String sessionId, int index);
     void notifyAudioAttributesChanged(String sessionId, in ParcelImpl attrs);
     void notifyVideoSizeChanged(String sessionId, in ParcelImpl videoSize);
+    boolean surfaceExists(String sessionId);
 
     void setPlaylist(String sessionId, in List<ParcelImpl> playlist);
     void createAndSetDummyPlaylist(String sessionId, int size);
diff --git a/media2/session/version-compat-tests/current/client/build.gradle b/media2/session/version-compat-tests/current/client/build.gradle
index 97ac2c2..d2be13b 100644
--- a/media2/session/version-compat-tests/current/client/build.gradle
+++ b/media2/session/version-compat-tests/current/client/build.gradle
@@ -28,6 +28,7 @@
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
 }
 
 android {
diff --git a/media2/session/version-compat-tests/current/client/src/androidTest/AndroidManifest.xml b/media2/session/version-compat-tests/current/client/src/androidTest/AndroidManifest.xml
index 211dcff..ca16534 100644
--- a/media2/session/version-compat-tests/current/client/src/androidTest/AndroidManifest.xml
+++ b/media2/session/version-compat-tests/current/client/src/androidTest/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="androidx.media2.test.client.test">
     <application android:supportsRtl="true">
+        <activity android:name="androidx.media2.test.client.SurfaceActivity" />
+
         <service android:name="androidx.media2.test.client.MediaControllerProviderService">
             <intent-filter>
                 <!-- Keep sync with CommonConstants.java -->
diff --git a/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/RemoteMediaSession.java b/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/RemoteMediaSession.java
index c248273..67cd431 100644
--- a/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/RemoteMediaSession.java
+++ b/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/RemoteMediaSession.java
@@ -505,6 +505,10 @@
                 Log.e(TAG, "Failed to call notifyVideoSizeChanged()");
             }
         }
+
+        public boolean surfaceExists() throws RemoteException {
+            return mBinder.surfaceExists(mSessionId);
+        }
     }
 
     ////////////////////////////////////////////////////////////////////////////////
diff --git a/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/SurfaceActivity.java b/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/SurfaceActivity.java
new file mode 100644
index 0000000..d2e6d81
--- /dev/null
+++ b/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/SurfaceActivity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.media2.test.client;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import androidx.media2.test.client.test.R;
+
+public class SurfaceActivity extends Activity {
+    private SurfaceHolder mHolder;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_surface);
+
+        SurfaceView surfaceView = findViewById(R.id.surface_view);
+        mHolder = surfaceView.getHolder();
+    }
+
+    public SurfaceHolder getSurfaceHolder() {
+        return mHolder;
+    }
+}
diff --git a/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaController_SurfaceTest.java b/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaController_SurfaceTest.java
new file mode 100644
index 0000000..401d4b6
--- /dev/null
+++ b/media2/session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaController_SurfaceTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.media2.test.client.tests;
+
+import static android.content.Context.KEYGUARD_SERVICE;
+
+import static androidx.media2.test.common.CommonConstants.DEFAULT_TEST_NAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
+import android.os.Build;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.media2.session.MediaController;
+import androidx.media2.session.SessionResult;
+import androidx.media2.test.client.RemoteMediaSession;
+import androidx.media2.test.client.SurfaceActivity;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaController#setSurface(Surface)}.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class MediaController_SurfaceTest extends MediaSessionTestBase {
+    private static final String TAG = "MC_SurfaceTest";
+
+    private Instrumentation mInstrumentation;
+    private SurfaceActivity mActivity;
+    private RemoteMediaSession mRemoteSession;
+
+    @Rule
+    public ActivityTestRule<SurfaceActivity> mActivityRule =
+            new ActivityTestRule<>(SurfaceActivity.class);
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mActivity = mActivityRule.getActivity();
+
+        setKeepScreenOn();
+
+        mRemoteSession = new RemoteMediaSession(DEFAULT_TEST_NAME, mContext, null);
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+
+        mRemoteSession.cleanUp();
+    }
+
+    @Test
+    public void testSetSurface() throws Exception {
+        prepareLooper();
+        MediaController controller = createController(mRemoteSession.getToken());
+
+        // Set
+        final Surface testSurface = mActivity.getSurfaceHolder().getSurface();
+        SessionResult result = controller.setSurface(testSurface)
+                .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertEquals(SessionResult.RESULT_SUCCESS, result.getResultCode());
+        assertTrue(mRemoteSession.getMockPlayer().surfaceExists());
+
+        // Reset
+        result = controller.setSurface(null).get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertEquals(SessionResult.RESULT_SUCCESS, result.getResultCode());
+        assertFalse(mRemoteSession.getMockPlayer().surfaceExists());
+    }
+
+    private void setKeepScreenOn() throws Exception {
+        try {
+            setKeepScreenOnOrThrow();
+        } catch (Throwable tr) {
+            throw new Exception(tr);
+        }
+    }
+
+    private void setKeepScreenOnOrThrow() throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (Build.VERSION.SDK_INT >= 27) {
+                    mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                    mActivity.setTurnScreenOn(true);
+                    mActivity.setShowWhenLocked(true);
+                    KeyguardManager keyguardManager = (KeyguardManager)
+                            mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
+                    keyguardManager.requestDismissKeyguard(mActivity, null);
+                } else {
+                    mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                            | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                            | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                            | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+                }
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+    }
+}
diff --git a/media2/session/version-compat-tests/current/client/src/androidTest/res/layout/activity_surface.xml b/media2/session/version-compat-tests/current/client/src/androidTest/res/layout/activity_surface.xml
new file mode 100644
index 0000000..7945f97
--- /dev/null
+++ b/media2/session/version-compat-tests/current/client/src/androidTest/res/layout/activity_surface.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
+    <SurfaceView
+        android:id="@+id/surface_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    </SurfaceView>
+</LinearLayout>
diff --git a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MediaSessionProviderService.java b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MediaSessionProviderService.java
index c797948..cefed2d 100644
--- a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MediaSessionProviderService.java
+++ b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MediaSessionProviderService.java
@@ -527,5 +527,12 @@
             VideoSize videoSizeObj = MediaParcelUtils.fromParcelable(videoSize);
             player.notifyVideoSizeChanged(videoSizeObj);
         }
+
+        @Override
+        public boolean surfaceExists(String sessionId) {
+            MediaSession session = mSessionMap.get(sessionId);
+            MockPlayer player = (MockPlayer) session.getPlayer();
+            return player.surfaceExists();
+        }
     }
 }
diff --git a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockPlayer.java b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockPlayer.java
index a1087ae..d259c0a 100644
--- a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockPlayer.java
+++ b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockPlayer.java
@@ -16,7 +16,10 @@
 
 package androidx.media2.test.service;
 
+import android.view.Surface;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.core.util.Pair;
 import androidx.media.AudioAttributesCompat;
 import androidx.media2.common.MediaItem;
@@ -61,6 +64,7 @@
     public @RepeatMode int mRepeatMode = -1;
     public @ShuffleMode int mShuffleMode = -1;
     public VideoSize mVideoSize = new VideoSize(0, 0);
+    public Surface mSurface;
 
     public boolean mSetPlaylistCalled;
     public boolean mUpdatePlaylistMetadataCalled;
@@ -523,4 +527,15 @@
             });
         }
     }
+
+    @Override
+    @NonNull
+    public ListenableFuture<PlayerResult> setSurfaceInternal(@Nullable Surface surface) {
+        mSurface = surface;
+        return new SyncListenableFuture(mCurrentMediaItem);
+    }
+
+    public boolean surfaceExists() {
+        return mSurface != null;
+    }
 }
diff --git a/media2/widget/build.gradle b/media2/widget/build.gradle
index 31eb89f..7b6af85 100644
--- a/media2/widget/build.gradle
+++ b/media2/widget/build.gradle
@@ -27,7 +27,6 @@
     api(project(":media2:media2-common"))
     api(project(":media2:media2-player"))
     api(project(":media2:media2-session"))
-    implementation(project(":mediarouter"))
     implementation("androidx.appcompat:appcompat:1.0.2")
     implementation("androidx.palette:palette:1.0.0")
     implementation(project(":concurrent:concurrent-futures"))
diff --git a/media2/widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java b/media2/widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
index 88a83cd..647c1eb 100644
--- a/media2/widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
+++ b/media2/widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
@@ -280,7 +280,7 @@
     }
 
     @Test
-    public void testGetMetadataFromMusic() throws Throwable {
+    public void testGetMetadataFromMusicFile() throws Throwable {
         Uri uri = Uri.parse("android.resource://" + mContext.getPackageName() + "/"
                 + R.raw.test_music);
         AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(R.raw.test_music);
@@ -296,6 +296,11 @@
                 .setFileDescriptorLength(afd.getLength())
                 .build();
         afd.close();
+        // onCurrentMediaItemChanged is expected to be called 3 times:
+        //   1) after VideoView#setMediaItem is called.
+        //   2) after MediaSessionImplBase updates duration metadata by calling
+        //      MediaItem#setMetadata.
+        //   3) after VideoView extracts metadata and calls MediaItem#setMetadata.
         final CountDownLatch latchForUri = new CountDownLatch(3);
         final CountDownLatch latchForFile = new CountDownLatch(3);
         final MediaController controller =
@@ -349,6 +354,36 @@
     }
 
     @Test
+    public void testButtonVisibilityForMusicFile() throws Throwable {
+        Uri uri = Uri.parse("android.resource://" + mContext.getPackageName() + "/"
+                + R.raw.test_music);
+        final MediaItem uriMediaItem = createTestMediaItem2(uri);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaController controller =
+                createController(new MediaController.ControllerCallback() {
+                    @NonNull
+                    @Override
+                    public SessionResult onCustomCommand(@NonNull MediaController controller,
+                            @NonNull SessionCommand command, @Nullable Bundle args) {
+                        if (command.getCustomAction()
+                                == MediaControlView.EVENT_UPDATE_TRACK_STATUS) {
+                            latch.countDown();
+                        }
+                        return new SessionResult(SessionResult.RESULT_SUCCESS, null);
+                    }
+                });
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mVideoView.setMediaItem(uriMediaItem);
+            }
+        });
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        onView(withId(R.id.subtitle)).check(matches(not(isDisplayed())));
+    }
+
+    @Test
     public void testUpdateAndSelectSubtitleTrack() throws Throwable {
         Uri uri = Uri.parse("android.resource://" + mContext.getPackageName() + "/"
                 + R.raw.testvideo_with_2_subtitle_tracks);
@@ -446,7 +481,7 @@
                         });
             }
         });
-        onView(withId(R.id.fullscreen)).check(matches(isDisplayed()));
+        onView(withId(R.id.fullscreen)).check(matches(isCompletelyDisplayed()));
 
         onView(withId(R.id.fullscreen)).perform(click());
         assertTrue(latchOn.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
diff --git a/media2/widget/src/main/java/androidx/media2/widget/MediaControlView.java b/media2/widget/src/main/java/androidx/media2/widget/MediaControlView.java
index 1a30d1e..7f8b189 100644
--- a/media2/widget/src/main/java/androidx/media2/widget/MediaControlView.java
+++ b/media2/widget/src/main/java/androidx/media2/widget/MediaControlView.java
@@ -66,8 +66,6 @@
 import androidx.media2.session.SessionCommandGroup;
 import androidx.media2.session.SessionResult;
 import androidx.media2.session.SessionToken;
-import androidx.mediarouter.app.MediaRouteButton;
-import androidx.mediarouter.media.MediaRouteSelector;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -201,7 +199,6 @@
     private View mTitleBar;
     private TextView mTitleView;
     private View mAdExternalLink;
-    private MediaRouteButton mRouteButton;
 
     // Relating to Center View
     ViewGroup mCenterView;
@@ -526,16 +523,6 @@
         }
     }
 
-    void setRouteSelector(MediaRouteSelector selector) {
-        if (selector != null && !selector.isEmpty()) {
-            mRouteButton.setRouteSelector(selector);
-            mRouteButton.setVisibility(View.VISIBLE);
-        } else {
-            mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
-            mRouteButton.setVisibility(View.GONE);
-        }
-    }
-
     void setDelayedAnimationInterval(long interval) {
         mDelayedAnimationIntervalMs = interval;
     }
@@ -555,7 +542,6 @@
         mTitleBar = findViewById(R.id.title_bar);
         mTitleView = findViewById(R.id.title_text);
         mAdExternalLink = findViewById(R.id.ad_external_link);
-        mRouteButton = findViewById(R.id.cast);
 
         // Relating to Center View
         mCenterView = findViewById(R.id.center_view);
diff --git a/media2/widget/src/main/java/androidx/media2/widget/RoutePlayer.java b/media2/widget/src/main/java/androidx/media2/widget/RoutePlayer.java
deleted file mode 100644
index b51bd3d..0000000
--- a/media2/widget/src/main/java/androidx/media2/widget/RoutePlayer.java
+++ /dev/null
@@ -1,517 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.media2.widget;
-
-import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_BAD_VALUE;
-import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_INVALID_STATE;
-import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_UNKNOWN;
-import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.concurrent.futures.ResolvableFuture;
-import androidx.core.util.Pair;
-import androidx.media.AudioAttributesCompat;
-import androidx.media2.common.MediaItem;
-import androidx.media2.common.MediaMetadata;
-import androidx.media2.common.SessionPlayer;
-import androidx.media2.common.UriMediaItem;
-import androidx.media2.session.RemoteSessionPlayer;
-import androidx.mediarouter.media.MediaItemStatus;
-import androidx.mediarouter.media.MediaRouteSelector;
-import androidx.mediarouter.media.MediaRouter;
-import androidx.mediarouter.media.MediaSessionStatus;
-import androidx.mediarouter.media.RemotePlaybackClient;
-import androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback;
-import androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback;
-import androidx.mediarouter.media.RemotePlaybackClient.StatusCallback;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
-
-/* package */ class RoutePlayer extends RemoteSessionPlayer {
-    private static final String TAG = "RoutePlayer";
-    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    private static final int ITEM_NONE = -1;
-
-    String mItemId;
-    int mCurrentPlayerState;
-    long mDuration;
-    long mLastStatusChangedTime;
-    long mPosition;
-    boolean mCanResume;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    MediaRouter.RouteInfo mSelectedRoute;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final List<ResolvableFuture<PlayerResult>> mPendingVolumeResult = new ArrayList<>();
-
-    private MediaItem mItem;
-    private MediaRouter mMediaRouter;
-    private RemotePlaybackClient mClient;
-
-    private MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
-        @Override
-        public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
-            if (TextUtils.equals(route.getId(), mSelectedRoute.getId())) {
-                final int volume = route.getVolume();
-                for (int i = 0; i < mPendingVolumeResult.size(); i++) {
-                    mPendingVolumeResult.get(i).set(new PlayerResult(
-                            RESULT_SUCCESS, getCurrentMediaItem()));
-                }
-                mPendingVolumeResult.clear();
-                List<Pair<PlayerCallback, Executor>> callbacks = getCallbacks();
-                for (Pair<PlayerCallback, Executor> pair : callbacks) {
-                    if (pair.first instanceof RemoteSessionPlayer.Callback) {
-                        final RemoteSessionPlayer.PlayerCallback callback = pair.first;
-                        pair.second.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                ((RemoteSessionPlayer.Callback) callback)
-                                        .onVolumeChanged(RoutePlayer.this, volume);
-                            }
-                        });
-                    }
-                }
-            }
-        }
-    };
-
-    private StatusCallback mStatusCallback = new StatusCallback() {
-        @Override
-        public void onItemStatusChanged(Bundle data,
-                String sessionId, MediaSessionStatus sessionStatus,
-                String itemId, MediaItemStatus itemStatus) {
-            if (DEBUG && !isSessionActive(sessionStatus)) {
-                Log.v(TAG, "onItemStatusChanged() is called, but session is not active.");
-            }
-            mLastStatusChangedTime = SystemClock.elapsedRealtime();
-            mPosition = itemStatus.getContentPosition();
-            mCurrentPlayerState = convertPlaybackStateToPlayerState(itemStatus.getPlaybackState());
-
-            List<Pair<PlayerCallback, Executor>> callbacks = getCallbacks();
-            for (Pair<PlayerCallback, Executor> pair : callbacks) {
-                final PlayerCallback callback = pair.first;
-                pair.second.execute(new Runnable() {
-                    @Override
-                    public void run() {
-                        callback.onPlayerStateChanged(RoutePlayer.this, mCurrentPlayerState);
-                    }
-                });
-            }
-        }
-    };
-
-    public RoutePlayer(Context context, MediaRouteSelector selector,
-            MediaRouter.RouteInfo route) {
-        mMediaRouter = MediaRouter.getInstance(context);
-        mMediaRouter.addCallback(selector, mRouterCallback);
-        mSelectedRoute = route;
-
-        mClient = new RemotePlaybackClient(context, route);
-        mClient.setStatusCallback(mStatusCallback);
-        if (mClient.isSessionManagementSupported()) {
-            mClient.startSession(null, new SessionActionCallback() {
-                @Override
-                public void onResult(Bundle data,
-                        String sessionId, MediaSessionStatus sessionStatus) {
-                    if (DEBUG && !isSessionActive(sessionStatus)) {
-                        Log.v(TAG, "RoutePlayer has been initialized, but session is not"
-                                + "active.");
-                    }
-                }
-            });
-        }
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> play() {
-        if (mItem == null) {
-            return createResult(RESULT_ERROR_BAD_VALUE);
-        }
-
-        // RemotePlaybackClient cannot call resume(..) without calling pause(..) first.
-        if (!mCanResume) {
-            return playInternal();
-        }
-
-        if (mClient.isSessionManagementSupported()) {
-            final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
-            mClient.resume(null, new SessionActionCallback() {
-                @Override
-                public void onResult(Bundle data,
-                        String sessionId, MediaSessionStatus sessionStatus) {
-                    if (DEBUG && !isSessionActive(sessionStatus)) {
-                        Log.v(TAG, "play() is called, but session is not active.");
-                    }
-                    // Do nothing since this returns the buffering state--
-                    // StatusCallback#onItemStatusChanged is called when the session reaches the
-                    // play state.
-                    result.set(new PlayerResult(RESULT_SUCCESS, getCurrentMediaItem()));
-                }
-            });
-        }
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> prepare() {
-        return createResult();
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> pause() {
-        if (mClient.isSessionManagementSupported()) {
-            final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
-            mClient.pause(null, new SessionActionCallback() {
-                @Override
-                public void onResult(Bundle data,
-                        String sessionId, MediaSessionStatus sessionStatus) {
-                    if (DEBUG && !isSessionActive(sessionStatus)) {
-                        Log.v(TAG, "pause() is called, but session is not active.");
-                    }
-                    mCanResume = true;
-                    // Do not update playback state here since this returns the buffering state--
-                    // StatusCallback#onItemStatusChanged is called when the session reaches the
-                    // pause state.
-                    result.set(new PlayerResult(RESULT_SUCCESS, getCurrentMediaItem()));
-                }
-            });
-        }
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> seekTo(long pos) {
-        if (mClient.isSessionManagementSupported()) {
-            final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
-            mClient.seek(mItemId, pos, null, new ItemActionCallback() {
-                @Override
-                public void onResult(Bundle data,
-                        String sessionId, MediaSessionStatus sessionStatus,
-                        String itemId, final MediaItemStatus itemStatus) {
-                    if (DEBUG && !isSessionActive(sessionStatus)) {
-                        Log.v(TAG, "seekTo(long) is called, but session is not active.");
-                    }
-                    if (itemStatus != null) {
-                        List<Pair<PlayerCallback, Executor>> callbacks = getCallbacks();
-                        for (Pair<PlayerCallback, Executor> pair : callbacks) {
-                            final PlayerCallback callback = pair.first;
-                            pair.second.execute(new Runnable() {
-                                @Override
-                                public void run() {
-                                    callback.onSeekCompleted(RoutePlayer.this,
-                                            itemStatus.getContentPosition());
-                                }
-                            });
-                        }
-                    } else {
-                        result.set(new PlayerResult(RESULT_ERROR_UNKNOWN,
-                                getCurrentMediaItem()));
-                    }
-                }
-            });
-        }
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public long getCurrentPosition() {
-        long expectedPosition = mPosition;
-        if (mCurrentPlayerState == PLAYER_STATE_PLAYING) {
-            expectedPosition = mPosition + (SystemClock.elapsedRealtime() - mLastStatusChangedTime);
-        }
-        return expectedPosition;
-    }
-
-    @Override
-    public long getDuration() {
-        return mDuration;
-    }
-
-    @Override
-    public long getBufferedPosition() {
-        return 0;
-    }
-
-    @Override
-    public int getPlayerState() {
-        return mCurrentPlayerState;
-    }
-
-    @Override
-    public int getBufferingState() {
-        return SessionPlayer.BUFFERING_STATE_UNKNOWN;
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> setAudioAttributes(AudioAttributesCompat attributes) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public AudioAttributesCompat getAudioAttributes() {
-        return null;
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> setMediaItem(MediaItem item) {
-        mItem = item;
-        return createResult();
-    }
-
-    @Override
-    public MediaItem getCurrentMediaItem() {
-        return mItem;
-    }
-
-    @Override
-    public int getCurrentMediaItemIndex() {
-        return ITEM_NONE;
-    }
-
-    @Override
-    public int getPreviousMediaItemIndex() {
-        return ITEM_NONE;
-    }
-
-    @Override
-    public int getNextMediaItemIndex() {
-        return ITEM_NONE;
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> setPlaybackSpeed(float speed) {
-        // Do nothing
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public float getPlaybackSpeed() {
-        return 1.0f;
-    }
-
-    @Override
-    public int getVolume() {
-        return mSelectedRoute.getVolume();
-    }
-
-    @Override
-    public Future<PlayerResult> adjustVolume(int direction) {
-        mSelectedRoute.requestUpdateVolume(direction);
-
-        ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
-        mPendingVolumeResult.add(result);
-        return result;
-    }
-
-    @Override
-    public Future<PlayerResult> setVolume(int volume) {
-        mSelectedRoute.requestSetVolume(volume);
-
-        ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
-        mPendingVolumeResult.add(result);
-        return result;
-    }
-
-    @Override
-    public int getMaxVolume() {
-        return mSelectedRoute.getVolumeMax();
-    }
-
-    @Override
-    public int getVolumeControlType() {
-        return mSelectedRoute.getVolumeHandling();
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> setPlaylist(List<MediaItem> list,
-            MediaMetadata metadata) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> addPlaylistItem(int index, MediaItem item) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> removePlaylistItem(int index) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> replacePlaylistItem(int index, MediaItem item) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> skipToPreviousPlaylistItem() {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> skipToNextPlaylistItem() {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> skipToPlaylistItem(int index) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> updatePlaylistMetadata(MediaMetadata metadata) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> setRepeatMode(int repeatMode) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public ListenableFuture<PlayerResult> setShuffleMode(int shuffleMode) {
-        // TODO: implement
-        return createResult(RESULT_ERROR_INVALID_STATE);
-    }
-
-    @Override
-    public List<MediaItem> getPlaylist() {
-        List<MediaItem> list = new ArrayList<>();
-        list.add(mItem);
-        return list;
-    }
-
-    @Override
-    public MediaMetadata getPlaylistMetadata() {
-        return null;
-    }
-
-    @Override
-    public int getRepeatMode() {
-        return SessionPlayer.REPEAT_MODE_NONE;
-    }
-
-    @Override
-    public int getShuffleMode() {
-        return SessionPlayer.SHUFFLE_MODE_NONE;
-    }
-
-    @Override
-    public void close() {
-        if (mClient != null) {
-            try {
-                mClient.release();
-            } catch (IllegalArgumentException e) {
-                Log.d(TAG, "Receiver not registered");
-            }
-            mClient = null;
-        }
-        mMediaRouter.removeCallback(mRouterCallback);
-    }
-
-    void setCurrentPosition(long position) {
-        mPosition = position;
-    }
-
-    boolean isSessionActive(MediaSessionStatus status) {
-        if (status == null || status.getSessionState() == MediaSessionStatus.SESSION_STATE_ENDED
-                || status.getSessionState() == MediaSessionStatus.SESSION_STATE_INVALIDATED) {
-            return false;
-        }
-        return true;
-    }
-
-    int convertPlaybackStateToPlayerState(int playbackState) {
-        int playerState = PLAYER_STATE_IDLE;
-        switch (playbackState) {
-            case MediaItemStatus.PLAYBACK_STATE_PENDING:
-            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
-            case MediaItemStatus.PLAYBACK_STATE_CANCELED:
-                playerState = PLAYER_STATE_IDLE;
-                break;
-            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
-                playerState = PLAYER_STATE_PLAYING;
-                break;
-            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
-            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
-                playerState = PLAYER_STATE_PAUSED;
-                break;
-            case MediaItemStatus.PLAYBACK_STATE_INVALIDATED:
-            case MediaItemStatus.PLAYBACK_STATE_ERROR:
-                playerState =  PLAYER_STATE_ERROR;
-                break;
-        }
-        return playerState;
-    }
-
-    private ListenableFuture<PlayerResult> playInternal() {
-        if (!(mItem instanceof UriMediaItem)) {
-            Log.w(TAG, "Data source type is not Uri." + mItem);
-            return createResult(RESULT_ERROR_BAD_VALUE);
-        }
-        final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
-        mClient.play(((UriMediaItem) mItem).getUri(), "video/mp4", null, mPosition, null,
-                new ItemActionCallback() {
-                    @Override
-                    public void onResult(Bundle data, String sessionId,
-                            MediaSessionStatus sessionStatus,
-                            String itemId, MediaItemStatus itemStatus) {
-                        if (DEBUG && !isSessionActive(sessionStatus)) {
-                            Log.v(TAG, "play() is called, but session is not active.");
-                        }
-                        mItemId = itemId;
-                        if (itemStatus != null) {
-                            mDuration = itemStatus.getContentDuration();
-                        }
-                        // Do not update playback state here since this returns the buffering state.
-                        // StatusCallback#onItemStatusChanged is called when the session reaches the
-                        // play state.
-                        result.set(new PlayerResult(RESULT_SUCCESS, getCurrentMediaItem()));
-                    }
-                });
-        return result;
-    }
-
-    private ListenableFuture<PlayerResult> createResult() {
-        return createResult(RESULT_SUCCESS);
-    }
-
-    private ListenableFuture<PlayerResult> createResult(int code) {
-        ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
-        result.set(new PlayerResult(code, getCurrentMediaItem()));
-        return result;
-    }
-}
diff --git a/media2/widget/src/main/java/androidx/media2/widget/VideoView.java b/media2/widget/src/main/java/androidx/media2/widget/VideoView.java
index 2c38673..bceb17b 100644
--- a/media2/widget/src/main/java/androidx/media2/widget/VideoView.java
+++ b/media2/widget/src/main/java/androidx/media2/widget/VideoView.java
@@ -57,14 +57,10 @@
 import androidx.media2.player.subtitle.SubtitleTrack;
 import androidx.media2.session.MediaController;
 import androidx.media2.session.MediaSession;
-import androidx.media2.session.RemoteSessionPlayer;
 import androidx.media2.session.SessionCommand;
 import androidx.media2.session.SessionCommandGroup;
 import androidx.media2.session.SessionResult;
 import androidx.media2.session.SessionToken;
-import androidx.mediarouter.media.MediaControlIntent;
-import androidx.mediarouter.media.MediaRouteSelector;
-import androidx.mediarouter.media.MediaRouter;
 import androidx.palette.graphics.Palette;
 
 import java.lang.annotation.Retention;
@@ -197,58 +193,6 @@
 
     SubtitleAnchorView mSubtitleAnchorView;
 
-    private MediaRouter mMediaRouter;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-            MediaRouteSelector mRouteSelector;
-    MediaRouter.RouteInfo mRoute;
-    RoutePlayer mRoutePlayer;
-
-    private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
-        @Override
-        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
-            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
-                // Save local playback state and position
-                int localPlaybackState = mCurrentState;
-                long localPlaybackPosition = (mMediaSession == null)
-                        ? 0 : mMediaSession.getPlayer().getCurrentPosition();
-
-                // Update player
-                resetPlayer();
-                mRoute = route;
-                mRoutePlayer = new RoutePlayer(getContext(), mRouteSelector, route);
-                // TODO: Replace with MediaSession#setPlaylist once b/110811730 is fixed.
-                mRoutePlayer.setMediaItem(mMediaItem);
-                mRoutePlayer.setCurrentPosition(localPlaybackPosition);
-                ensureSessionWithPlayer(mRoutePlayer);
-                if (localPlaybackState == STATE_PLAYING) {
-                    mMediaSession.getPlayer().play();
-                }
-            }
-        }
-
-        @Override
-        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
-            long currentPosition = 0;
-            int currentState = 0;
-            if (mRoute != null && mRoutePlayer != null) {
-                currentPosition = mRoutePlayer.getCurrentPosition();
-                currentState = mRoutePlayer.getPlayerState();
-                mRoutePlayer.close();
-                mRoutePlayer = null;
-            }
-            if (mRoute == route) {
-                mRoute = null;
-            }
-            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
-                openVideo();
-                mMediaSession.getPlayer().seekTo(currentPosition);
-                if (currentState == SessionPlayer.PLAYER_STATE_PLAYING) {
-                    mMediaSession.getPlayer().play();
-                }
-            }
-        }
-    };
-
     private final VideoViewInterface.SurfaceListener mSurfaceListener =
             new VideoViewInterface.SurfaceListener() {
         @Override
@@ -381,12 +325,6 @@
             mCurrentView = mTextureView;
         }
         mTargetView = mCurrentView;
-
-        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
-        builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
-        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
-        mRouteSelector = builder.build();
     }
 
     /**
@@ -545,11 +483,6 @@
         ensureSessionWithPlayer(mMediaPlayer);
 
         attachMediaControlView();
-        mMediaRouter = MediaRouter.getInstance(getContext());
-        // TODO: Revisit once ag/4207152 is merged.
-        mMediaRouter.setMediaSessionCompat(mMediaSession.getSessionCompat());
-        mMediaRouter.addCallback(mRouteSelector, mRouterCallback,
-                MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
     }
 
     @Override
@@ -652,7 +585,7 @@
 
     boolean needToStart() {
         return mMediaSession != null
-                && (mMediaPlayer != null || mRoutePlayer != null) && isWaitingPlayback();
+                && mMediaPlayer != null && isWaitingPlayback();
     }
 
     private boolean isWaitingPlayback() {
@@ -666,10 +599,6 @@
         }
         if (mMediaItem != null) {
             resetPlayer();
-            if (isRemotePlayback()) {
-                mRoutePlayer.setMediaItem(mMediaItem);
-                return;
-            }
         }
 
         try {
@@ -752,12 +681,6 @@
         }
     }
 
-    boolean isRemotePlayback() {
-        return mRoutePlayer != null
-                && mMediaSession != null
-                && (mMediaSession.getPlayer() instanceof RemoteSessionPlayer);
-    }
-
     void selectSubtitleTrack(TrackInfo trackInfo) {
         if (!isMediaPrepared()) {
             return;
@@ -1017,16 +940,6 @@
                         task.execute();
                     }
 
-                    if (mMediaControlView != null) {
-                        Uri uri = (mMediaItem instanceof UriMediaItem)
-                                ? ((UriMediaItem) mMediaItem).getUri() : null;
-                        if (uri != null && UriUtil.isFromNetwork(uri)) {
-                            mMediaControlView.setRouteSelector(mRouteSelector);
-                        } else {
-                            mMediaControlView.setRouteSelector(null);
-                        }
-                    }
-
                     if (player instanceof MediaPlayer) {
                         if (needToStart()) {
                             mMediaSession.getPlayer().play();
@@ -1096,10 +1009,6 @@
                     Log.w(TAG, "onCustomCommand() is ignored. session is already gone.");
                 }
             }
-            if (isRemotePlayback()) {
-                // TODO: call mRoutePlayer.onCommand()
-                return new SessionResult(RESULT_SUCCESS, null);
-            }
             switch (command.getCustomAction()) {
                 case MediaControlView.COMMAND_SHOW_SUBTITLE:
                     int indexInSubtitleTrackList = args != null ? args.getInt(
diff --git a/media2/widget/src/main/res/layout/media_controller.xml b/media2/widget/src/main/res/layout/media_controller.xml
index bc49531..7228241 100644
--- a/media2/widget/src/main/res/layout/media_controller.xml
+++ b/media2/widget/src/main/res/layout/media_controller.xml
@@ -101,11 +101,6 @@
                     android:layout_height="wrap_content"
                     style="@style/TitleBarButton.Launch" />
             </LinearLayout>
-
-            <view class="androidx.mediarouter.app.MediaRouteButton"
-                android:id="@+id/cast"
-                android:visibility="gone"
-                style="@style/TitleBarButton" />
         </LinearLayout>
     </LinearLayout>
 
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
new file mode 100644
index 0000000..25b3700
--- /dev/null
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/DialogFragmentNavigatorTest.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.navigation.fragment
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentFactory
+import androidx.fragment.app.FragmentManager
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class DialogFragmentNavigatorTest {
+
+    companion object {
+        private const val INITIAL_FRAGMENT = 1
+    }
+
+    @get:Rule
+    var activityRule = ActivityTestRule(EmptyActivity::class.java)
+
+    private lateinit var emptyActivity: EmptyActivity
+    private lateinit var fragmentManager: FragmentManager
+
+    @Before
+    fun setup() {
+        emptyActivity = activityRule.activity
+        fragmentManager = emptyActivity.supportFragmentManager
+    }
+
+    @UiThreadTest
+    @Test
+    fun testNavigate() {
+        lateinit var dialogFragment: DialogFragment
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragment = fragment
+                    }
+                }
+            }
+        }
+        val dialogNavigator = DialogFragmentNavigator(emptyActivity, fragmentManager)
+        val destination = dialogNavigator.createDestination().apply {
+            id = INITIAL_FRAGMENT
+            className = EmptyDialogFragment::class.java.name
+        }
+
+        assertThat(dialogNavigator.navigate(destination, null, null, null))
+            .isEqualTo(destination)
+        fragmentManager.executePendingTransactions()
+        assertWithMessage("Dialog should be shown")
+            .that(dialogFragment.requireDialog().isShowing)
+            .isTrue()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPop() {
+        lateinit var dialogFragment: DialogFragment
+        fragmentManager.fragmentFactory = object : FragmentFactory() {
+            override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+                return super.instantiate(classLoader, className).also { fragment ->
+                    if (fragment is DialogFragment) {
+                        dialogFragment = fragment
+                    }
+                }
+            }
+        }
+        val dialogNavigator = DialogFragmentNavigator(emptyActivity, fragmentManager)
+        val destination = dialogNavigator.createDestination().apply {
+            id = INITIAL_FRAGMENT
+            className = EmptyDialogFragment::class.java.name
+        }
+
+        assertThat(dialogNavigator.navigate(destination, null, null, null))
+            .isEqualTo(destination)
+        fragmentManager.executePendingTransactions()
+        assertWithMessage("Dialog should be shown")
+            .that(dialogFragment.requireDialog().isShowing)
+            .isTrue()
+        assertWithMessage("DialogNavigator should pop dialog off the back stack")
+            .that(dialogNavigator.popBackStack())
+            .isTrue()
+        assertWithMessage("Pop should dismiss the DialogFragment")
+            .that(dialogFragment.requireDialog().isShowing)
+            .isFalse()
+    }
+}
+
+class EmptyDialogFragment : DialogFragment() {
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
+        AlertDialog.Builder(requireContext()).create()
+}
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/OnBackPressedTest.kt b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/OnBackPressedTest.kt
index e3dc4c6..e79d900 100644
--- a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/OnBackPressedTest.kt
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/OnBackPressedTest.kt
@@ -65,6 +65,23 @@
 
     @UiThreadTest
     @Test
+    fun testOnBackPressedAfterNavigate_notDefaultNavHost() {
+        val activity = activityRule.activity
+        val navController = activity.navController
+        navController.setGraph(R.navigation.nav_simple)
+        navController.navigate(R.id.empty_fragment)
+        activity.supportFragmentManager.beginTransaction()
+            .setPrimaryNavigationFragment(null)
+            .commitNow()
+
+        activity.onBackPressed()
+        assertWithMessage("onBackPressed() should finish the activity when not the primary nav")
+            .that(activity.isFinishing)
+            .isTrue()
+    }
+
+    @UiThreadTest
+    @Test
     fun testOnBackPressedWithChildBackStack() {
         val activity = activityRule.activity
         val navHostFragment = activity.supportFragmentManager.primaryNavigationFragment
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java
index 38da7ab..0fd7840 100644
--- a/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigator.java
@@ -81,7 +81,7 @@
             return false;
         }
         Fragment existingFragment = mFragmentManager
-                .findFragmentByTag(DIALOG_TAG + mDialogCount--);
+                .findFragmentByTag(DIALOG_TAG + --mDialogCount);
         if (existingFragment != null) {
             existingFragment.getLifecycle().removeObserver(mObserver);
             ((DialogFragment) existingFragment).dismiss();
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
index 8a344e9..551f124 100644
--- a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
@@ -121,6 +121,8 @@
     }
 
     private NavController mNavController;
+    private NavController.NavHostOnBackPressedManager mOnBackPressedManager;
+    private Boolean mIsPrimaryBeforeOnCreate = null;
 
     // State that will be saved and restored
     private int mGraphId;
@@ -207,7 +209,13 @@
 
         mNavController = new NavController(context);
         mNavController.setHostLifecycleOwner(this);
-        mNavController.setHostOnBackPressedDispatcherOwner(requireActivity());
+        mOnBackPressedManager = mNavController
+                .setHostOnBackPressedDispatcherOwner(requireActivity());
+        // Set the default state - this will be updated whenever
+        // onPrimaryNavigationFragmentChanged() is called
+        mOnBackPressedManager.enableOnBackPressed(
+                mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
+        mIsPrimaryBeforeOnCreate = null;
         mNavController.setHostViewModelStore(getViewModelStore());
         onCreateNavController(mNavController);
 
@@ -262,6 +270,16 @@
         navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
     }
 
+    @CallSuper
+    @Override
+    public void onPrimaryNavigationFragmentChanged(boolean isPrimaryNavigationFragment) {
+        if (mOnBackPressedManager != null) {
+            mOnBackPressedManager.enableOnBackPressed(isPrimaryNavigationFragment);
+        } else {
+            mIsPrimaryBeforeOnCreate = isPrimaryNavigationFragment;
+        }
+    }
+
     /**
      * Create the FragmentNavigator that this NavHostFragment will use. By default, this uses
      * {@link FragmentNavigator}, which replaces the entire contents of the NavHostFragment.
diff --git a/navigation/runtime/api/2.1.0-alpha04.txt b/navigation/runtime/api/2.1.0-alpha04.txt
index 8fe3dc4..8517257 100644
--- a/navigation/runtime/api/2.1.0-alpha04.txt
+++ b/navigation/runtime/api/2.1.0-alpha04.txt
@@ -69,11 +69,15 @@
     method @CallSuper public void setGraph(androidx.navigation.NavGraph);
     method @CallSuper public void setGraph(androidx.navigation.NavGraph, android.os.Bundle?);
     method public void setHostLifecycleOwner(androidx.lifecycle.LifecycleOwner);
-    method public void setHostOnBackPressedDispatcherOwner(androidx.activity.OnBackPressedDispatcherOwner);
+    method public androidx.navigation.NavController.NavHostOnBackPressedManager setHostOnBackPressedDispatcherOwner(androidx.activity.OnBackPressedDispatcherOwner);
     method public void setHostViewModelStore(androidx.lifecycle.ViewModelStore);
     field public static final String KEY_DEEP_LINK_INTENT = "android-support-nav:controller:deepLinkIntent";
   }
 
+  public static interface NavController.NavHostOnBackPressedManager {
+    method public void enableOnBackPressed(boolean);
+  }
+
   public static interface NavController.OnDestinationChangedListener {
     method public void onDestinationChanged(androidx.navigation.NavController, androidx.navigation.NavDestination, android.os.Bundle?);
   }
diff --git a/navigation/runtime/api/current.txt b/navigation/runtime/api/current.txt
index 8fe3dc4..8517257 100644
--- a/navigation/runtime/api/current.txt
+++ b/navigation/runtime/api/current.txt
@@ -69,11 +69,15 @@
     method @CallSuper public void setGraph(androidx.navigation.NavGraph);
     method @CallSuper public void setGraph(androidx.navigation.NavGraph, android.os.Bundle?);
     method public void setHostLifecycleOwner(androidx.lifecycle.LifecycleOwner);
-    method public void setHostOnBackPressedDispatcherOwner(androidx.activity.OnBackPressedDispatcherOwner);
+    method public androidx.navigation.NavController.NavHostOnBackPressedManager setHostOnBackPressedDispatcherOwner(androidx.activity.OnBackPressedDispatcherOwner);
     method public void setHostViewModelStore(androidx.lifecycle.ViewModelStore);
     field public static final String KEY_DEEP_LINK_INTENT = "android-support-nav:controller:deepLinkIntent";
   }
 
+  public static interface NavController.NavHostOnBackPressedManager {
+    method public void enableOnBackPressed(boolean);
+  }
+
   public static interface NavController.OnDestinationChangedListener {
     method public void onDestinationChanged(androidx.navigation.NavController, androidx.navigation.NavDestination, android.os.Bundle?);
   }
diff --git a/navigation/runtime/src/main/java/androidx/navigation/NavController.java b/navigation/runtime/src/main/java/androidx/navigation/NavController.java
index 82467e2..4d2dc31 100644
--- a/navigation/runtime/src/main/java/androidx/navigation/NavController.java
+++ b/navigation/runtime/src/main/java/androidx/navigation/NavController.java
@@ -104,6 +104,7 @@
             popBackStack();
         }
     };
+    private boolean mEnableOnBackPressedCallback = true;
 
     /**
      * OnDestinationChangedListener receives a callback when the
@@ -125,6 +126,22 @@
     }
 
     /**
+     * Interface returned by
+     * {@link #setHostOnBackPressedDispatcherOwner(OnBackPressedDispatcherOwner)} to
+     * allow the {@link NavHost} to manually disable or enable whether the NavController
+     * should actively handle system Back button events.
+     */
+    public interface NavHostOnBackPressedManager {
+        /**
+         * Set whether the NavController should handle the system Back button events via the
+         * registered {@link OnBackPressedDispatcher}.
+         *
+         * @param enabled True if the NavController should handle system Back button events.
+         */
+        void enableOnBackPressed(boolean enabled);
+    }
+
+    /**
      * Constructs a new controller for a given {@link Context}. Controllers should not be
      * used outside of their context and retain a hard reference to the context supplied.
      * If you need a global controller, pass {@link Context#getApplicationContext()}.
@@ -287,7 +304,7 @@
                 break;
             }
         }
-        mOnBackPressedCallback.setEnabled(getDestinationCountOnBackStack() > 1);
+        updateOnBackPressedCallbackEnabled();
         return popped;
     }
 
@@ -485,7 +502,7 @@
                 }
                 mBackStack.add(new NavBackStackEntry(uuid, node, args));
             }
-            mOnBackPressedCallback.setEnabled(getDestinationCountOnBackStack() > 1);
+            updateOnBackPressedCallbackEnabled();
             mBackStackUUIDsToRestore = null;
             mBackStackIdsToRestore = null;
             mBackStackArgsToRestore = null;
@@ -872,7 +889,7 @@
                     newDest.addInDefaultArgs(finalArgs));
             mBackStack.add(newBackStackEntry);
         }
-        mOnBackPressedCallback.setEnabled(getDestinationCountOnBackStack() > 1);
+        updateOnBackPressedCallbackEnabled();
         if (popped || newDest != null) {
             dispatchOnDestinationChanged();
         }
@@ -1010,9 +1027,14 @@
      *
      * @param owner The {@link OnBackPressedDispatcherOwner} associated with the containing
      * {@link NavHost}.
+     * @return a {@link NavHostOnBackPressedManager} that allows you to enable or disable
+     * whether this NavController should intercept the system Back button events using this
+     * {@link OnBackPressedDispatcher}.
      * @see #setHostLifecycleOwner(LifecycleOwner)
      */
-    public void setHostOnBackPressedDispatcherOwner(@NonNull OnBackPressedDispatcherOwner owner) {
+    @NonNull
+    public NavHostOnBackPressedManager setHostOnBackPressedDispatcherOwner(
+            @NonNull OnBackPressedDispatcherOwner owner) {
         if (mLifecycleOwner == null) {
             mLifecycleOwner = owner;
         }
@@ -1021,6 +1043,23 @@
         mOnBackPressedCallback.remove();
         // Then add it to the new dispatcher
         dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
+        return new NavHostOnBackPressedManager() {
+            @Override
+            public void enableOnBackPressed(boolean enabled) {
+                setEnableOnBackPressedCallback(enabled);
+            }
+        };
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void setEnableOnBackPressedCallback(boolean enableOnBackPressedCallback) {
+        mEnableOnBackPressedCallback = enableOnBackPressedCallback;
+        updateOnBackPressedCallbackEnabled();
+    }
+
+    private void updateOnBackPressedCallbackEnabled() {
+        mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
+                && getDestinationCountOnBackStack() > 1);
     }
 
     /**
diff --git a/palette/build.gradle b/palette/build.gradle
index 5f7cc1f..a537d09 100644
--- a/palette/build.gradle
+++ b/palette/build.gradle
@@ -15,7 +15,7 @@
 
 dependencies {
     api(project(":core"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     annotationProcessor(NULLAWAY)
 
diff --git a/preference/api/1.1.0-alpha06.txt b/preference/api/1.1.0-alpha06.txt
index 0edd4f7b..5afea33 100644
--- a/preference/api/1.1.0-alpha06.txt
+++ b/preference/api/1.1.0-alpha06.txt
@@ -49,7 +49,6 @@
     ctor public EditTextPreference(android.content.Context!, android.util.AttributeSet!, int);
     ctor public EditTextPreference(android.content.Context!, android.util.AttributeSet!);
     ctor public EditTextPreference(android.content.Context!);
-    method public androidx.preference.EditTextPreference.OnBindEditTextListener? getOnBindEditTextListener();
     method public String! getText();
     method public void setOnBindEditTextListener(androidx.preference.EditTextPreference.OnBindEditTextListener?);
     method public void setText(String!);
diff --git a/preference/api/current.txt b/preference/api/current.txt
index 0edd4f7b..5afea33 100644
--- a/preference/api/current.txt
+++ b/preference/api/current.txt
@@ -49,7 +49,6 @@
     ctor public EditTextPreference(android.content.Context!, android.util.AttributeSet!, int);
     ctor public EditTextPreference(android.content.Context!, android.util.AttributeSet!);
     ctor public EditTextPreference(android.content.Context!);
-    method public androidx.preference.EditTextPreference.OnBindEditTextListener? getOnBindEditTextListener();
     method public String! getText();
     method public void setOnBindEditTextListener(androidx.preference.EditTextPreference.OnBindEditTextListener?);
     method public void setText(String!);
diff --git a/preference/src/androidTest/java/androidx/preference/tests/EditTextPreferenceTest.java b/preference/src/androidTest/java/androidx/preference/tests/EditTextPreferenceTest.java
index af6ef57..9091896 100644
--- a/preference/src/androidTest/java/androidx/preference/tests/EditTextPreferenceTest.java
+++ b/preference/src/androidTest/java/androidx/preference/tests/EditTextPreferenceTest.java
@@ -24,7 +24,6 @@
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertNull;
 
 import android.text.InputFilter;
 import android.widget.EditText;
@@ -76,8 +75,6 @@
         mActivityRule.runOnUiThread(new Runnable() {
             @Override
             public void run() {
-                assertNull(mEditTextPreference.getOnBindEditTextListener());
-
                 mEditTextPreference.setOnBindEditTextListener(
                         new EditTextPreference.OnBindEditTextListener() {
                             @Override
diff --git a/preference/src/main/java/androidx/preference/EditTextPreference.java b/preference/src/main/java/androidx/preference/EditTextPreference.java
index b597f0c..725c93a 100644
--- a/preference/src/main/java/androidx/preference/EditTextPreference.java
+++ b/preference/src/main/java/androidx/preference/EditTextPreference.java
@@ -158,7 +158,7 @@
      * there is no OnBindEditTextListener set
      * @see OnBindEditTextListener
      */
-    public @Nullable OnBindEditTextListener getOnBindEditTextListener() {
+    @Nullable OnBindEditTextListener getOnBindEditTextListener() {
         return mOnBindEditTextListener;
     }
 
diff --git a/recyclerview/selection/build.gradle b/recyclerview/selection/build.gradle
index b63fc0b..7f59362 100644
--- a/recyclerview/selection/build.gradle
+++ b/recyclerview/selection/build.gradle
@@ -27,7 +27,7 @@
     api(project(":recyclerview"))
     api("androidx.annotation:annotation:1.1.0-rc01")
     api(project(":core"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/slices/core/build.gradle b/slices/core/build.gradle
index 615a1f2..8e7c2ad 100644
--- a/slices/core/build.gradle
+++ b/slices/core/build.gradle
@@ -27,7 +27,7 @@
 dependencies {
     implementation "androidx.annotation:annotation:1.1.0-rc01"
     implementation project(":appcompat")
-    api project(":collection")
+    api "androidx.collection:collection:1.1.0-rc01"
     api(project(":remotecallback"))
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/slices/view/build.gradle b/slices/view/build.gradle
index 0ee765c..92d9bd9 100644
--- a/slices/view/build.gradle
+++ b/slices/view/build.gradle
@@ -26,7 +26,7 @@
 dependencies {
     implementation(project(":slice-core"))
     implementation(project(":recyclerview"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
     api(ARCH_LIFECYCLE_LIVEDATA_CORE, libs.exclude_annotations_transitive)
 
     androidTestImplementation(project(":slice-builders"))
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/LocaleTestUtilsTest.kt b/testutils/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
similarity index 74%
rename from viewpager2/src/androidTest/java/androidx/viewpager2/LocaleTestUtilsTest.kt
rename to testutils/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
index a10fd14..433d6de 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/LocaleTestUtilsTest.kt
+++ b/testutils/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.viewpager2
+package androidx.testutils
 
 import android.content.Context
 import android.content.res.Configuration
@@ -25,10 +25,10 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.CoreMatchers
 import org.hamcrest.Matchers
 import org.junit.After
-import org.junit.Assert.assertThat
+import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -40,7 +40,8 @@
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class LocaleTestUtilsTest {
-    private val configuration: Configuration get() =
+    private val configuration: Configuration
+        get() =
         (ApplicationProvider.getApplicationContext() as Context).resources.configuration
     private val Configuration.language: String get() =
         ConfigurationCompat.getLocales(this).get(0).toString()
@@ -51,7 +52,8 @@
     @Before
     fun setUp() {
         localeUtil = LocaleTestUtils(
-            ApplicationProvider.getApplicationContext() as android.content.Context)
+            ApplicationProvider.getApplicationContext() as Context
+        )
         determineDefaultLayoutDirection()
     }
 
@@ -84,12 +86,24 @@
         val getReason: (String, String) -> String = { name, code ->
             "$name test language '$code' does not exist on test device"
         }
-        assertThat(getReason("Default", LocaleTestUtils.DEFAULT_TEST_LANGUAGE),
-            LocaleTestUtils.DEFAULT_TEST_LANGUAGE, Matchers.isIn(availableLanguages))
-        assertThat(getReason("LTR", LocaleTestUtils.LTR_LANGUAGE),
-            LocaleTestUtils.LTR_LANGUAGE, Matchers.isIn(availableLanguages))
-        assertThat(getReason("RTL", LocaleTestUtils.RTL_LANGUAGE),
-            LocaleTestUtils.RTL_LANGUAGE, Matchers.isIn(availableLanguages))
+        Assert.assertThat(
+            getReason(
+                "Default",
+                LocaleTestUtils.DEFAULT_TEST_LANGUAGE
+            ),
+            LocaleTestUtils.DEFAULT_TEST_LANGUAGE,
+            Matchers.isIn(availableLanguages)
+        )
+        Assert.assertThat(
+            getReason("LTR", LocaleTestUtils.LTR_LANGUAGE),
+            LocaleTestUtils.LTR_LANGUAGE,
+            Matchers.isIn(availableLanguages)
+        )
+        Assert.assertThat(
+            getReason("RTL", LocaleTestUtils.RTL_LANGUAGE),
+            LocaleTestUtils.RTL_LANGUAGE,
+            Matchers.isIn(availableLanguages)
+        )
     }
 
     private fun assertDefaultValues() {
@@ -97,25 +111,25 @@
     }
 
     private fun assertLocaleIs(lang: String, expectRtl: Boolean) {
-        assertThat(
+        Assert.assertThat(
             "Locale should be $lang",
             configuration.language,
-            equalTo(lang)
+            CoreMatchers.equalTo(lang)
         )
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-            assertThat(
+            Assert.assertThat(
                 "Layout direction should be ${if (expectRtl) "RTL" else "LTR"}",
                 configuration.layoutDirection,
-                equalTo(if (expectRtl) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR)
+                CoreMatchers.equalTo(if (expectRtl) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR)
             )
         }
     }
 
     private fun determineDefaultLayoutDirection() {
-        assertThat(
+        Assert.assertThat(
             "Locale must still be the default when determining the default layout direction",
             configuration.language,
-            equalTo(DEFAULT_LANGUAGE)
+            CoreMatchers.equalTo(DEFAULT_LANGUAGE)
         )
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
             expectRtlInDefaultLanguage = configuration.layoutDirection == LAYOUT_DIRECTION_RTL
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/LocaleTestUtils.kt b/testutils/src/main/java/androidx/testutils/LocaleTestUtils.kt
similarity index 98%
rename from viewpager2/src/androidTest/java/androidx/viewpager2/LocaleTestUtils.kt
rename to testutils/src/main/java/androidx/testutils/LocaleTestUtils.kt
index 6ca6392..e2e4c4e 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/LocaleTestUtils.kt
+++ b/testutils/src/main/java/androidx/testutils/LocaleTestUtils.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.viewpager2
+package androidx.testutils
 
 import android.content.Context
 import android.content.res.Configuration
diff --git a/ui/android-view/src/main/java/androidx/ui/androidview/WebComponentActivity.kt b/ui/android-view/src/main/java/androidx/ui/androidview/WebComponentActivity.kt
index e4b2611..333e390 100644
--- a/ui/android-view/src/main/java/androidx/ui/androidview/WebComponentActivity.kt
+++ b/ui/android-view/src/main/java/androidx/ui/androidview/WebComponentActivity.kt
@@ -54,7 +54,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        setContent @Composable {
+        setContent {
             if (WebContext.debug) {
                 Log.e("WebCompAct", "setContent")
             }
diff --git a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloAnimationActivity.kt b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloAnimationActivity.kt
index 17fad61..bcb86e2 100644
--- a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloAnimationActivity.kt
+++ b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloAnimationActivity.kt
@@ -89,7 +89,7 @@
                 recompose()
             }
         }, (200..800).random().toLong())
-        Transition(definition = definition, toState = toState) @Composable { state ->
+        Transition(definition = definition, toState = toState) { state ->
             DrawColorRectState(state = state)
         }
     }
diff --git a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloGestureBasedAnimationActivity.kt b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloGestureBasedAnimationActivity.kt
index 2b10222..a098e3c 100644
--- a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloGestureBasedAnimationActivity.kt
+++ b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/HelloGestureBasedAnimationActivity.kt
@@ -76,7 +76,7 @@
          toState.value = ComponentState.Released },
          toState.value = ComponentState.Released }) {
         val children = @Composable {
-            Transition(definition = definition, toState = toState.value) @Composable { state ->
+            Transition(definition = definition, toState = toState.value) { state ->
                 DrawScaledRect(scale=state[scale], color=state[color])
             }
         }
diff --git a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/RepeatedRotationActivity.kt b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/RepeatedRotationActivity.kt
index c331d7f..f0836cf 100644
--- a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/RepeatedRotationActivity.kt
+++ b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/RepeatedRotationActivity.kt
@@ -73,7 +73,7 @@
                     Transition(
                         definition = definition,
                         toState = state.value
-                    ) @Composable { state ->
+                    ) { state ->
                         Draw { canvas, parentSize ->
                             canvas.save()
                             canvas.rotate(radians(state[rotation]))
diff --git a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/StateBasedRippleAnimation.kt b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/StateBasedRippleAnimation.kt
index ac21c95..333bdd7 100644
--- a/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/StateBasedRippleAnimation.kt
+++ b/ui/animation/integration-tests/animation-demos/src/main/java/androidx/ui/animation/demos/StateBasedRippleAnimation.kt
@@ -93,7 +93,7 @@
         }
         PressGestureDetector(  {
             val children = @Composable {
-                Transition(definition = rippleTransDef, toState = toState) @Composable { state ->
+                Transition(definition = rippleTransDef, toState = toState) { state ->
                     RippleRectFromState(state = state)
                 }
             }
diff --git a/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/AnimationGestureSemanticsActivity.kt b/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/AnimationGestureSemanticsActivity.kt
index d1c9504..2011e3b 100644
--- a/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/AnimationGestureSemanticsActivity.kt
+++ b/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/AnimationGestureSemanticsActivity.kt
@@ -161,11 +161,11 @@
     @Suppress("FunctionName")
     @Composable
     private fun Animation(animationEndState: ComponentState) {
-        Layout(children = @Composable {
+        Layout(children = {
             Transition(
                 definition = transitionDefinition,
                 toState = animationEndState
-            ) @Composable { state ->
+            ) { state ->
                 Circle(color = state[colorKey], sizeRatio = state[sizeKey])
             }
         }, layoutBlock = { _, constraints ->
diff --git a/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/VectorGraphicsActivity.kt b/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/VectorGraphicsActivity.kt
index f9ac57b..96f4de0 100644
--- a/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/VectorGraphicsActivity.kt
+++ b/ui/framework/integration-tests/framework-demos/src/main/java/androidx/ui/framework/demos/VectorGraphicsActivity.kt
@@ -38,7 +38,7 @@
         super.onCreate(savedInstanceState)
 
         val res = getResources()
-        setContent @Composable {
+        setContent {
             composer.registerAdapter { parent, child ->
                 adoptVectorGraphic(parent, child)
             }
diff --git a/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt b/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt
index 5f406e8..55bfbd8 100644
--- a/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt
+++ b/ui/framework/src/androidTest/java/androidx/ui/core/test/AndroidLayoutDrawTest.kt
@@ -278,7 +278,7 @@
         runOnUiThread {
             activity.setContent {
                 CraneWrapper {
-                    WithConstraints @Composable { constraints ->
+                    WithConstraints { constraints ->
                         topConstraints.value = constraints
                         Padding(size = size) {
                             WithConstraints { constraints ->
@@ -400,7 +400,7 @@
                         paint.color = model.outerColor
                         canvas.drawRect(parentSize.toRect(), paint)
                     }
-                    Layout(children = @Composable {
+                    Layout(children = {
                         AtLeastSize(size = model.size) {
                             Draw { canvas, parentSize ->
                                 drawLatch.countDown()
@@ -563,7 +563,7 @@
                 CraneWrapper {
                     Draw(children = {
                         AtLeastSize(size = (model.size * 3)) {
-                            Draw(children = @Composable {
+                            Draw(children = {
                                 Draw { canvas, parentSize ->
                                     val paint = Paint()
                                     paint.color = model.innerColor
diff --git a/ui/framework/src/main/java/androidx/ui/core/Layout.kt b/ui/framework/src/main/java/androidx/ui/core/Layout.kt
index b93cd0d..a599cbb 100644
--- a/ui/framework/src/main/java/androidx/ui/core/Layout.kt
+++ b/ui/framework/src/main/java/androidx/ui/core/Layout.kt
@@ -380,7 +380,7 @@
         val addMarkers = childrenArray.size > 1
         childrenArray.forEach { childrenComposable ->
             childrenComposable()
-            if (addMarkers) ChildrenEndMarker(p1 = childrenComposable)
+            if (addMarkers) ChildrenEndMarker(childrenComposable)
         }
     }
 
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/AlignTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/AlignTest.kt
index f6d84fa..7a2f924 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/AlignTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/AlignTest.kt
@@ -53,7 +53,7 @@
         val alignPosition = Ref<PxPosition>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.BottomRight) {
                 SaveLayoutInfo(
                     size = alignSize,
@@ -93,7 +93,7 @@
         val alignPosition = Ref<PxPosition>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Layout(
                 children = {
                     Align(alignment = Alignment.BottomRight) {
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/ConstrainedBoxTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/ConstrainedBoxTest.kt
index 77fc1c8..9642a1a 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/ConstrainedBoxTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/ConstrainedBoxTest.kt
@@ -50,7 +50,7 @@
         val constrainedBoxSize = Ref<PxSize>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     constrainedBoxSize.value = coordinates.size
@@ -84,7 +84,7 @@
         val constrainedBoxSize = Ref<PxSize>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 Container(width = sizeDp, height = sizeDp) {
                     OnChildPositioned( coordinates ->
@@ -120,7 +120,7 @@
 
         val positionedLatch = CountDownLatch(1)
         val constrainedBoxSize = Ref<PxSize>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     constrainedBoxSize.value = coordinates.size
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/ContainerTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/ContainerTest.kt
index 5269ca9..4109f34 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/ContainerTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/ContainerTest.kt
@@ -61,7 +61,7 @@
 
         val positionedLatch = CountDownLatch(1)
         val containerSize = Ref<PxSize>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     containerSize.value = coordinates.size
@@ -88,7 +88,7 @@
         val positionedLatch = CountDownLatch(2)
         val containerSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     containerSize.value = coordinates.size
@@ -127,7 +127,7 @@
         val positionedLatch = CountDownLatch(4)
         val containerSize = Ref<PxSize>()
         val childSize = Array(3) { PxSize(0.px, 0.px) }
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     containerSize.value = coordinates.size
@@ -187,7 +187,7 @@
         val containerSize = Ref<PxSize>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnPositioned( coordinates ->
                     alignSize.value = coordinates.size
@@ -231,7 +231,7 @@
         val containerSize = Ref<PxSize>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     containerSize.value = coordinates.size
@@ -270,7 +270,7 @@
 
         val containerSize = Ref<PxSize>()
         val latch = CountDownLatch(1)
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 Container(width = sizeDp, height = sizeDp, padding = EdgeInsets(10.dp)) {
                     OnPositioned( coordinates ->
@@ -303,7 +303,7 @@
 
         var containerSize: PxSize? = null
         val latch = CountDownLatch(1)
-        show @Composable {
+        show {
             Wrap {
                 Container(padding = edgeInsets) {
                     FixedSpacer(width = childSizeDp, height = childSizeDp)
@@ -328,7 +328,7 @@
 
         var childCoordinates: LayoutCoordinates? = null
         val latch = CountDownLatch(1)
-        show @Composable {
+        show {
             Wrap {
                 Container(width = containerSize, height = containerSize, padding = edgeInsets) {
                     OnChildPositioned( coordinates ->
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/FlexTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/FlexTest.kt
index 19bd6e1..77ce33b 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/FlexTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/FlexTest.kt
@@ -60,7 +60,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Row {
                     Container(width = sizeDp, height = sizeDp) {
@@ -109,7 +109,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 FlexRow {
                     val widthDp = 50.px.toDp()
@@ -172,7 +172,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 FlexRow {
                     flexible(flex = 1f) {
@@ -228,7 +228,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Column {
                     Container(width = sizeDp, height = sizeDp) {
@@ -276,7 +276,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 FlexColumn {
                     val heightDp = 50.px.toDp()
@@ -339,7 +339,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 FlexColumn {
                     flexible(flex = 1f) {
@@ -395,7 +395,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Row(crossAxisAlignment = CrossAxisAlignment.Start) {
                     Container(width = sizeDp, height = sizeDp) {
@@ -444,7 +444,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Row(crossAxisAlignment = CrossAxisAlignment.End) {
                     Container(width = sizeDp, height = sizeDp) {
@@ -496,7 +496,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Row(crossAxisAlignment = CrossAxisAlignment.Stretch) {
                     Container(width = sizeDp, height = sizeDp) {
@@ -539,7 +539,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Column(crossAxisAlignment = CrossAxisAlignment.Start) {
                     Container(width = sizeDp, height = sizeDp) {
@@ -588,7 +588,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Column(crossAxisAlignment = CrossAxisAlignment.End) {
                     Container(width = sizeDp, height = sizeDp) {
@@ -641,7 +641,7 @@
         val drawLatch = CountDownLatch(2)
         val childSize = arrayOf(PxSize(-1.px, -1.px), PxSize(-1.px, -1.px))
         val childPosition = arrayOf(PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px))
-        show @Composable {
+        show {
             Center {
                 Column(crossAxisAlignment = CrossAxisAlignment.Stretch) {
                     Container(width = sizeDp, height = sizeDp) {
@@ -682,7 +682,7 @@
 
         val drawLatch = CountDownLatch(1)
         lateinit var rowSize: PxSize
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisSize = MainAxisSize.Max) {
                     FixedSpacer(width = sizeDp, height = sizeDp)
@@ -713,7 +713,7 @@
 
         val drawLatch = CountDownLatch(1)
         lateinit var rowSize: PxSize
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisSize = MainAxisSize.Min) {
                     FixedSpacer(width = sizeDp, height = sizeDp)
@@ -745,7 +745,7 @@
 
         val drawLatch = CountDownLatch(1)
         lateinit var rowSize: PxSize
-        show @Composable {
+        show {
             Center {
                 ConstrainedBox(constraints = DpConstraints(minWidth = rowWidthDp)) {
                     Row(mainAxisSize = MainAxisSize.Min) {
@@ -781,7 +781,7 @@
         val drawLatch = CountDownLatch(2)
         lateinit var rowSize: PxSize
         lateinit var expandedChildSize: PxSize
-        show @Composable {
+        show {
             Center {
                 ConstrainedBox(constraints = DpConstraints(minWidth = rowWidthDp)) {
                     FlexRow(mainAxisSize = MainAxisSize.Min) {
@@ -824,7 +824,7 @@
 
         val drawLatch = CountDownLatch(1)
         lateinit var columnSize: PxSize
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisSize = MainAxisSize.Max) {
                     FixedSpacer(width = sizeDp, height = sizeDp)
@@ -855,7 +855,7 @@
 
         val drawLatch = CountDownLatch(1)
         lateinit var columnSize: PxSize
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisSize = MainAxisSize.Min) {
                     FixedSpacer(width = sizeDp, height = sizeDp)
@@ -889,7 +889,7 @@
         val drawLatch = CountDownLatch(2)
         lateinit var columnSize: PxSize
         lateinit var expandedChildSize: PxSize
-        show @Composable {
+        show {
             Center {
                 ConstrainedBox(constraints = DpConstraints(minHeight = columnHeightDp)) {
                     FlexColumn(mainAxisSize = MainAxisSize.Min) {
@@ -934,7 +934,7 @@
 
         val drawLatch = CountDownLatch(1)
         lateinit var columnSize: PxSize
-        show @Composable {
+        show {
             Center {
                 ConstrainedBox(constraints = DpConstraints(minHeight = columnHeightDp)) {
                     Column(mainAxisSize = MainAxisSize.Min) {
@@ -970,7 +970,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisAlignment = MainAxisAlignment.Start) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1012,7 +1012,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisAlignment = MainAxisAlignment.End) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1054,7 +1054,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisAlignment = MainAxisAlignment.Center) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1097,7 +1097,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisAlignment = MainAxisAlignment.SpaceEvenly) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1140,7 +1140,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisAlignment = MainAxisAlignment.SpaceBetween) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1183,7 +1183,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Row(mainAxisAlignment = MainAxisAlignment.SpaceAround) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1226,7 +1226,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisAlignment = MainAxisAlignment.Start) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1268,7 +1268,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisAlignment = MainAxisAlignment.End) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1310,7 +1310,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisAlignment = MainAxisAlignment.Center) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1353,7 +1353,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisAlignment = MainAxisAlignment.SpaceEvenly) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1396,7 +1396,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisAlignment = MainAxisAlignment.SpaceBetween) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1439,7 +1439,7 @@
             PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px), PxPosition(-1.px, -1.px)
         )
         val childLayoutCoordinates = arrayOfNulls<LayoutCoordinates?>(childPosition.size)
-        show @Composable {
+        show {
             Center {
                 Column(mainAxisAlignment = MainAxisAlignment.SpaceAround) {
                     // TODO(popam): replace with normal for loop when IR is fixed
@@ -1480,7 +1480,7 @@
 
         val drawLatch = CountDownLatch(4)
         val containerSize = Ref<PxSize>()
-        show @Composable {
+        show {
             Center {
                 ConstrainedBox(
                     constraints = DpConstraints.tightConstraints(sizeDp, sizeDp)
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/OnPositionedTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/OnPositionedTest.kt
index 06e5b45..67e7bb3 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/OnPositionedTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/OnPositionedTest.kt
@@ -48,7 +48,7 @@
         var realTop: Px? = null
 
         val drawLatch = CountDownLatch(1)
-        show @Composable {
+        show {
             Padding(left = paddingLeftPx.toDp(), top = paddingTopPx.toDp()) {
                 Container(expanded = true) {
                     OnPositioned(>
@@ -73,7 +73,7 @@
         var realTop: Px? = null
 
         val drawLatch = CountDownLatch(1)
-        show @Composable {
+        show {
             Padding(left = paddingLeftPx.toDp(), top = paddingTopPx.toDp()) {
                 OnChildPositioned(>
                     realLeft = it.position.x
@@ -99,7 +99,7 @@
         var childCoordinates: LayoutCoordinates? = null
 
         val drawLatch = CountDownLatch(2)
-        show @Composable {
+        show {
             Padding(left = firstPaddingPx.toDp()) {
                 Padding(left = secondPaddingPx.toDp()) {
                     OnPositioned(>
@@ -137,7 +137,7 @@
         var secondCoordinates: LayoutCoordinates? = null
 
         val drawLatch = CountDownLatch(2)
-        show @Composable {
+        show {
             Row {
                 OnChildPositioned(>
                     firstCoordinates = it
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/PaddingTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/PaddingTest.kt
index da6979d..cd91a7c 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/PaddingTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/PaddingTest.kt
@@ -149,7 +149,7 @@
         val drawLatch = CountDownLatch(1)
         var childSize = PxSize(-1.px, -1.px)
         var childPosition = PxPosition(-1.px, -1.px)
-        show @Composable {
+        show {
             Center {
                 ConstrainedBox(constraints = DpConstraints.tightConstraints(sizeDp, sizeDp)) {
                     val children = @Composable {
@@ -162,7 +162,7 @@
                             })
                         }
                     }
-                    paddingContainer(p1 = children)
+                    paddingContainer(children)
                 }
             }
         }
@@ -194,7 +194,7 @@
         val drawLatch = CountDownLatch(1)
         var childSize = PxSize(-1.px, -1.px)
         var childPosition = PxPosition(-1.px, -1.px)
-        show @Composable {
+        show {
             Center {
                 ConstrainedBox(constraints = DpConstraints.tightConstraints(sizeDp, sizeDp)) {
                     val children = @Composable {
@@ -207,7 +207,7 @@
                             })
                         }
                     }
-                    paddingContainer(p1 = children)
+                    paddingContainer(children)
                 }
             }
         }
@@ -247,7 +247,7 @@
             val drawLatch = CountDownLatch(1)
             var childSize = PxSize(-1.px, -1.px)
             var childPosition = PxPosition(-1.px, -1.px)
-            show @Composable {
+            show {
                 Center {
                     ConstrainedBox(constraints = DpConstraints.tightConstraints(sizeDp, sizeDp)) {
                         val children = @Composable {
@@ -260,7 +260,7 @@
                                 })
                             }
                         }
-                        paddingContainer(p1 = children)
+                        paddingContainer(children)
                     }
                 }
             }
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/SpacerTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/SpacerTest.kt
index b6446a2..147af44 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/SpacerTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/SpacerTest.kt
@@ -54,7 +54,7 @@
         val height = 71.dp
 
         val drawLatch = CountDownLatch(1)
-        show @Composable {
+        show {
             Container(constraints = bigConstraints) {
                 OnChildPositioned( position ->
                     size = position.size
@@ -81,7 +81,7 @@
         val drawLatch = CountDownLatch(1)
         val containerWidth = 5.dp
         val containerHeight = 7.dp
-        show @Composable {
+        show {
             Center {
                 Container(
                     constraints = DpConstraints(
@@ -112,7 +112,7 @@
         val width = 71.dp
 
         val drawLatch = CountDownLatch(1)
-        show @Composable {
+        show {
             Container(constraints = bigConstraints) {
                 OnChildPositioned( position ->
                     size = position.size
@@ -138,7 +138,7 @@
         val drawLatch = CountDownLatch(1)
         val containerWidth = 5.dp
         val containerHeight = 7.dp
-        show @Composable {
+        show {
             Center {
                 Container(
                     constraints = DpConstraints(
@@ -169,7 +169,7 @@
         val height = 7.dp
 
         val drawLatch = CountDownLatch(1)
-        show @Composable {
+        show {
             Container(constraints = bigConstraints) {
                 OnChildPositioned( position ->
                     size = position.size
@@ -195,7 +195,7 @@
         val drawLatch = CountDownLatch(1)
         val containerWidth = 5.dp
         val containerHeight = 7.dp
-        show @Composable {
+        show {
             Center {
                 Container(
                     constraints = DpConstraints(
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/StackTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/StackTest.kt
index 8282148..3e04720 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/StackTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/StackTest.kt
@@ -52,7 +52,7 @@
         val alignedChildPosition = Ref<PxPosition>()
         val positionedChildSize = Ref<PxSize>()
         val positionedChildPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     stackSize.value = coordinates.size
@@ -106,7 +106,7 @@
         val stackSize = Ref<PxSize>()
         val childSize = arrayOf(Ref<PxSize>(), Ref<PxSize>())
         val childPosition = arrayOf(Ref<PxPosition>(), Ref<PxPosition>())
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     stackSize.value = coordinates.size
@@ -155,7 +155,7 @@
         val stackSize = Ref<PxSize>()
         val childSize = Array(7) { Ref<PxSize>() }
         val childPosition = Array(7) { Ref<PxPosition>() }
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     stackSize.value = coordinates.size
diff --git a/ui/layout/src/androidTest/java/androidx/ui/layout/test/WrapTest.kt b/ui/layout/src/androidTest/java/androidx/ui/layout/test/WrapTest.kt
index caa9ecc..f670aeb 100644
--- a/ui/layout/src/androidTest/java/androidx/ui/layout/test/WrapTest.kt
+++ b/ui/layout/src/androidTest/java/androidx/ui/layout/test/WrapTest.kt
@@ -51,7 +51,7 @@
         val wrapSize = Ref<PxSize>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     wrapSize.value = coordinates.size
@@ -87,7 +87,7 @@
         val wrapSize = Ref<PxSize>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     wrapSize.value = coordinates.size
@@ -127,7 +127,7 @@
         val wrapSize = Ref<PxSize>()
         val childSize = Ref<PxSize>()
         val childPosition = Ref<PxPosition>()
-        show @Composable {
+        show {
             Align(alignment = Alignment.TopLeft) {
                 OnChildPositioned( coordinates ->
                     wrapSize.value = coordinates.size
diff --git a/ui/layout/src/main/java/androidx/ui/layout/Flex.kt b/ui/layout/src/main/java/androidx/ui/layout/Flex.kt
index b801a00..a6c8832 100644
--- a/ui/layout/src/main/java/androidx/ui/layout/Flex.kt
+++ b/ui/layout/src/main/java/androidx/ui/layout/Flex.kt
@@ -45,7 +45,7 @@
         if (flex < 0) {
             throw IllegalArgumentException("flex must be >= 0")
         }
-        childrenList += @Composable {
+        childrenList += {
             ParentData(data = FlexInfo(flex = flex, fit = FlexFit.Tight), children = children)
         }
     }
@@ -54,13 +54,13 @@
         if (flex < 0) {
             throw IllegalArgumentException("flex must be >= 0")
         }
-        childrenList += @Composable {
+        childrenList += {
             ParentData(data = FlexInfo(flex = flex, fit = FlexFit.Loose), children = children)
         }
     }
 
     fun inflexible(children: @Composable() () -> Unit) {
-        childrenList += @Composable {
+        childrenList += {
             ParentData(data = FlexInfo(flex = 0f, fit = FlexFit.Loose), children = children)
         }
     }
@@ -485,7 +485,7 @@
     fun Placeable.mainAxisSize() = if (orientation == FlexOrientation.Horizontal) width else height
     fun Placeable.crossAxisSize() = if (orientation == FlexOrientation.Horizontal) height else width
 
-    val flexChildren: @Composable() () -> Unit = with(FlexChildren()) @Composable {
+    val flexChildren: @Composable() () -> Unit = with(FlexChildren()) {
         block()
         val composable = @Composable {
             childrenList.forEach { it() }
diff --git a/ui/layout/src/main/java/androidx/ui/layout/Stack.kt b/ui/layout/src/main/java/androidx/ui/layout/Stack.kt
index 9e3637a..d1d8e18 100644
--- a/ui/layout/src/main/java/androidx/ui/layout/Stack.kt
+++ b/ui/layout/src/main/java/androidx/ui/layout/Stack.kt
@@ -54,12 +54,12 @@
             leftInset = leftInset, topInset = topInset,
             rightInset = rightInset, bottomInset = bottomInset
         )
-        _stackChildren += @Composable { ParentData(data = data, children = children) }
+        _stackChildren += { ParentData(data = data, children = children) }
     }
 
     fun aligned(alignment: Alignment, children: @Composable() () -> Unit) {
         val data = StackChildData(alignment = alignment)
-        _stackChildren += @Composable { ParentData(data = data, children = children) }
+        _stackChildren += { ParentData(data = data, children = children) }
     }
 }
 
diff --git a/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/RippleActivity.kt b/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/RippleActivity.kt
index f066088..f00954e 100644
--- a/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/RippleActivity.kt
+++ b/ui/material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/RippleActivity.kt
@@ -34,8 +34,7 @@
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        // TODO: remove @Composable annotation here when b/131681875 is fixed
-        setContent @Composable {
+        setContent {
             val layoutParams = LinearLayout.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 0,
diff --git a/ui/material/src/main/java/androidx/ui/material/Checkbox.kt b/ui/material/src/main/java/androidx/ui/material/Checkbox.kt
index a8b63e4..a30d7a3 100644
--- a/ui/material/src/main/java/androidx/ui/material/Checkbox.kt
+++ b/ui/material/src/main/java/androidx/ui/material/Checkbox.kt
@@ -94,8 +94,7 @@
     val definition = +memo(activeColor, unselectedColor) {
         generateTransitionDefinition(activeColor, unselectedColor)
     }
-    // TODO: remove @Composable annotation here when b/131681875 is fixed
-    Transition(definition = definition, toState = value) @Composable { state ->
+    Transition(definition = definition, toState = value) { state ->
         DrawBox(
             color = state[BoxColorProp],
             innerRadiusFraction = state[InnerRadiusFractionProp]
diff --git a/ui/material/src/main/java/androidx/ui/material/RadioButton.kt b/ui/material/src/main/java/androidx/ui/material/RadioButton.kt
index e51952d..2b468bd 100644
--- a/ui/material/src/main/java/androidx/ui/material/RadioButton.kt
+++ b/ui/material/src/main/java/androidx/ui/material/RadioButton.kt
@@ -211,8 +211,7 @@
             val definition = +memo(selectedColor, unselectedColor) {
                 generateTransitionDefinition(selectedColor, unselectedColor)
             }
-            // TODO: remove @Composable annotation here when b/131681875 is fixed
-            Transition(definition = definition, toState = selected) @Composable { state ->
+            Transition(definition = definition, toState = selected) { state ->
                 DrawRadioButton(
                     color = state[ColorProp],
                     outerRadius = state[OuterRadiusProp],
diff --git a/ui/material/src/main/java/androidx/ui/material/Switch.kt b/ui/material/src/main/java/androidx/ui/material/Switch.kt
index a5d48c7..80d22d4 100644
--- a/ui/material/src/main/java/androidx/ui/material/Switch.kt
+++ b/ui/material/src/main/java/androidx/ui/material/Switch.kt
@@ -84,8 +84,7 @@
         (+themeColor { onSurface }).withOpacity(UncheckedTrackOpacity)
     }
     DrawTrack(color = trackColor)
-    // TODO: remove @Composable annotation here when b/131681875 is fixed
-    Transition(definition = transDef, toState = checked) @Composable { state ->
+    Transition(definition = transDef, toState = checked) { state ->
         DrawThumb(
             color = state[ThumbColorProp],
             relativePosition = state[RelativeThumbTranslationProp]
diff --git a/ui/test/src/main/java/androidx/ui/test/android/AndroidUiTestRunner.kt b/ui/test/src/main/java/androidx/ui/test/android/AndroidUiTestRunner.kt
index fb2f95c..abc21e1 100644
--- a/ui/test/src/main/java/androidx/ui/test/android/AndroidUiTestRunner.kt
+++ b/ui/test/src/main/java/androidx/ui/test/android/AndroidUiTestRunner.kt
@@ -175,7 +175,7 @@
 
     private fun setContentInternal(composable: @Composable() () -> Unit) {
         activity.setContentView(FrameLayout(activity).apply {
-            compositionContext = Compose.composeInto(this, null, composable = @Composable {
+            compositionContext = Compose.composeInto(this, null, composable = {
                 TestWrapper {
                     composable()
                 }
diff --git a/viewpager2/build.gradle b/viewpager2/build.gradle
index df96767..1408eb4 100644
--- a/viewpager2/build.gradle
+++ b/viewpager2/build.gradle
@@ -27,7 +27,7 @@
 dependencies {
     api(project(":fragment"))
     api(project(":recyclerview"))
-    implementation(project(":collection"))
+    implementation("androidx.collection:collection:1.1.0-rc01")
 
     androidTestImplementation(project(":appcompat"))
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
index 42d38bc..ba7dfdb 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
@@ -41,7 +41,7 @@
 import androidx.test.rule.ActivityTestRule
 import androidx.testutils.AppCompatActivityUtils
 import androidx.testutils.FragmentActivityUtils.waitForActivityDrawn
-import androidx.viewpager2.LocaleTestUtils
+import androidx.testutils.LocaleTestUtils
 import androidx.viewpager2.adapter.FragmentStateAdapter
 import androidx.viewpager2.test.R
 import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
@@ -84,7 +84,8 @@
     @Before
     open fun setUp() {
         localeUtil = LocaleTestUtils(
-            ApplicationProvider.getApplicationContext() as android.content.Context)
+            ApplicationProvider.getApplicationContext() as android.content.Context
+        )
         // Ensure a predictable test environment by explicitly setting a locale
         localeUtil.setLocale(LocaleTestUtils.DEFAULT_TEST_LANGUAGE)
     }
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FakeDragTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FakeDragTest.kt
index c3e93b9..a7a4e17 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FakeDragTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/FakeDragTest.kt
@@ -24,7 +24,7 @@
 import androidx.core.view.animation.PathInterpolatorCompat
 import androidx.test.filters.LargeTest
 import androidx.testutils.FragmentActivityUtils.waitForCycles
-import androidx.viewpager2.LocaleTestUtils
+import androidx.testutils.LocaleTestUtils
 import androidx.viewpager2.widget.BaseTest.Context.SwipeMethod
 import androidx.viewpager2.widget.FakeDragTest.Event.OnPageScrollStateChangedEvent
 import androidx.viewpager2.widget.FakeDragTest.Event.OnPageScrolledEvent
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/OffscreenPageLimitTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/OffscreenPageLimitTest.kt
index 6eab611..8fae152 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/OffscreenPageLimitTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/OffscreenPageLimitTest.kt
@@ -20,7 +20,7 @@
 import android.view.ViewGroup
 import androidx.recyclerview.widget.RecyclerView
 import androidx.test.filters.LargeTest
-import androidx.viewpager2.LocaleTestUtils
+import androidx.testutils.LocaleTestUtils
 import androidx.viewpager2.widget.OffscreenPageLimitTest.Event.OnChildViewAdded
 import androidx.viewpager2.widget.OffscreenPageLimitTest.Event.OnChildViewRemoved
 import androidx.viewpager2.widget.OffscreenPageLimitTest.Event.OnPageScrollStateChangedEvent
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
index fc0b31a..9dfefab 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/PageChangeCallbackTest.kt
@@ -22,9 +22,9 @@
 import androidx.recyclerview.widget.RecyclerView
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
+import androidx.testutils.LocaleTestUtils
 import androidx.testutils.PollingCheck
 import androidx.viewpager.widget.ViewPager
-import androidx.viewpager2.LocaleTestUtils
 import androidx.viewpager2.widget.BaseTest.Context.SwipeMethod
 import androidx.viewpager2.widget.PageChangeCallbackTest.Event.MarkerEvent
 import androidx.viewpager2.widget.PageChangeCallbackTest.Event.OnPageScrollStateChangedEvent
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
index 411285e..3b53100 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
@@ -17,8 +17,8 @@
 package androidx.viewpager2.widget.swipe
 
 import android.os.Bundle
+import androidx.testutils.LocaleTestUtils
 import androidx.testutils.RecreatedAppCompatActivity
-import androidx.viewpager2.LocaleTestUtils
 import androidx.viewpager2.test.R
 
 class TestActivity : RecreatedAppCompatActivity() {
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java
index 9720882..3af4c26 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/TestApplication.java
@@ -22,6 +22,8 @@
 import androidx.annotation.NonNull;
 import androidx.work.Configuration;
 
+import java.util.concurrent.Executors;
+
 /**
  * An Application class that initializes WorkManager.
  */
@@ -30,6 +32,8 @@
     @NonNull
     @Override
     public Configuration getWorkManagerConfiguration() {
-        return new Configuration.Builder().setMinimumLoggingLevel(Log.VERBOSE).build();
+        return new Configuration.Builder()
+                .setTaskExecutor(Executors.newCachedThreadPool())
+                .setMinimumLoggingLevel(Log.VERBOSE).build();
     }
 }
diff --git a/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt b/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
index 1948994..c67dfd2 100644
--- a/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
+++ b/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
@@ -75,10 +75,6 @@
                     mExecutor.execute(runnable)
                 }
 
-                override fun getBackgroundExecutorThread(): Thread {
-                    return Thread.currentThread()
-                }
-
                 override fun getBackgroundExecutor(): Executor {
                     return mExecutor
                 }
diff --git a/work/workmanager-ktx/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt b/work/workmanager-ktx/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
index 79a8825..f20e6f0 100644
--- a/work/workmanager-ktx/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
+++ b/work/workmanager-ktx/src/androidTest/java/androidx/work/CoroutineWorkerTest.kt
@@ -157,10 +157,6 @@
         override fun getBackgroundExecutor(): Executor {
             return mSynchronousExecutor
         }
-
-        override fun getBackgroundExecutorThread(): Thread {
-            return Thread.currentThread()
-        }
     }
 
     class SynchronousCoroutineWorker(context: Context, params: WorkerParameters) :
diff --git a/work/workmanager-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt b/work/workmanager-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt
index 722cf4e..54c9f47 100644
--- a/work/workmanager-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt
+++ b/work/workmanager-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt
@@ -154,9 +154,5 @@
         override fun getBackgroundExecutor(): Executor {
             return mSynchronousExecutor
         }
-
-        override fun getBackgroundExecutorThread(): Thread {
-            return Thread.currentThread()
-        }
     }
 }
diff --git a/work/workmanager-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java b/work/workmanager-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
index 6ef57d0..f727fd7 100644
--- a/work/workmanager-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
+++ b/work/workmanager-testing/src/main/java/androidx/work/testing/InstantWorkTaskExecutor.java
@@ -53,10 +53,4 @@
     public Executor getBackgroundExecutor() {
         return mSynchronousExecutor;
     }
-
-    @NonNull
-    @Override
-    public Thread getBackgroundExecutorThread() {
-        return Thread.currentThread();
-    }
 }
diff --git a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
index 434668f..d5cc128 100644
--- a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
+++ b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
@@ -74,12 +74,6 @@
                     public Executor getBackgroundExecutor() {
                         return mSynchronousExecutor;
                     }
-
-                    @NonNull
-                    @Override
-                    public Thread getBackgroundExecutorThread() {
-                        return Thread.currentThread();
-                    }
                 },
                 true);
 
diff --git a/work/workmanager/api/2.1.0-alpha01.txt b/work/workmanager/api/2.1.0-alpha01.txt
index 2c2382c..7b667ab 100644
--- a/work/workmanager/api/2.1.0-alpha01.txt
+++ b/work/workmanager/api/2.1.0-alpha01.txt
@@ -15,6 +15,7 @@
     method public java.util.concurrent.Executor getExecutor();
     method public int getMaxJobSchedulerId();
     method public int getMinJobSchedulerId();
+    method public java.util.concurrent.Executor getTaskExecutor();
     method public androidx.work.WorkerFactory getWorkerFactory();
     field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
   }
@@ -26,6 +27,7 @@
     method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
     method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
     method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
     method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
   }
 
diff --git a/work/workmanager/api/current.txt b/work/workmanager/api/current.txt
index 2c2382c..7b667ab 100644
--- a/work/workmanager/api/current.txt
+++ b/work/workmanager/api/current.txt
@@ -15,6 +15,7 @@
     method public java.util.concurrent.Executor getExecutor();
     method public int getMaxJobSchedulerId();
     method public int getMinJobSchedulerId();
+    method public java.util.concurrent.Executor getTaskExecutor();
     method public androidx.work.WorkerFactory getWorkerFactory();
     field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
   }
@@ -26,6 +27,7 @@
     method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
     method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
     method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
     method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
   }
 
diff --git a/work/workmanager/src/androidTest/java/androidx/work/DatabaseTest.java b/work/workmanager/src/androidTest/java/androidx/work/DatabaseTest.java
index 6448bb9..ca3a800 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/DatabaseTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/DatabaseTest.java
@@ -22,6 +22,8 @@
 import org.junit.After;
 import org.junit.Before;
 
+import java.util.concurrent.Executors;
+
 /**
  * An abstract class for getting an in-memory instance of the {@link WorkDatabase}.
  */
@@ -30,7 +32,10 @@
 
     @Before
     public void initializeDb() {
-        mDatabase = WorkDatabase.create(ApplicationProvider.getApplicationContext(), true);
+        mDatabase = WorkDatabase.create(
+                ApplicationProvider.getApplicationContext(),
+                Executors.newCachedThreadPool(),
+                true);
     }
 
     @After
diff --git a/work/workmanager/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java b/work/workmanager/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
index ce8a2cb..332139c 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
@@ -34,6 +34,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.Executor;
+
 @RunWith(AndroidJUnit4.class)
 public class DefaultWorkerFactoryTest extends DatabaseTest {
 
@@ -52,6 +54,7 @@
         OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
         insertWork(work);
 
+        Executor executor = new SynchronousExecutor();
         ListenableWorker worker = mDefaultWorkerFactory.createWorkerWithDefaultFallback(
                 mContext.getApplicationContext(),
                 TestWorker.class.getName(),
@@ -61,8 +64,8 @@
                         work.getTags(),
                         new WorkerParameters.RuntimeExtras(),
                         1,
-                        new SynchronousExecutor(),
-                        new WorkManagerTaskExecutor(),
+                        executor,
+                        new WorkManagerTaskExecutor(executor),
                         mDefaultWorkerFactory));
         assertThat(worker, is(notNullValue()));
         assertThat(worker,
diff --git a/work/workmanager/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt b/work/workmanager/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt
index 5026071..d84a892 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt
@@ -86,7 +86,7 @@
         WorkerParameters.RuntimeExtras(),
         1,
         SynchronousExecutor(),
-        WorkManagerTaskExecutor(),
+        WorkManagerTaskExecutor(SynchronousExecutor()),
         factory
     )
 }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
index d8613a1..c5b095e 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
@@ -101,7 +101,7 @@
         });
 
         Context context = ApplicationProvider.getApplicationContext();
-        mDatabase = WorkDatabase.create(context, true);
+        mDatabase = WorkDatabase.create(context, Executors.newCachedThreadPool(), true);
         InstantWorkTaskExecutor taskExecutor = new InstantWorkTaskExecutor();
         Configuration configuration = new Configuration.Builder()
                 .setExecutor(Executors.newSingleThreadExecutor())
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/SerialExecutorTest.kt b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/SerialExecutorTest.kt
new file mode 100644
index 0000000..53b872d
--- /dev/null
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/SerialExecutorTest.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.work.impl.utils
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.greaterThanOrEqualTo
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class SerialExecutorTest {
+
+    lateinit var executor: SerialExecutor
+
+    @Before
+    fun setUp() {
+        executor = SerialExecutor(Executors.newCachedThreadPool())
+    }
+
+    @Test
+    fun testSerialExecutor() {
+        val latch = CountDownLatch(3)
+        val first = TimestampTrackingRunnable(latch)
+        val second = TimestampTrackingRunnable(latch)
+        val third = TimestampTrackingRunnable(latch)
+        val commands = listOf(first, second, third)
+        commands.forEach(executor::execute)
+        latch.await(1, TimeUnit.SECONDS)
+        var lastStart = 0L
+        for (runnable in commands) {
+            assertThat(runnable.start, greaterThanOrEqualTo(lastStart))
+            lastStart = runnable.end
+        }
+    }
+
+    companion object {
+        class TimestampTrackingRunnable(private val latch: CountDownLatch) : Runnable {
+            var start: Long = 0
+            var end: Long = 0
+            override fun run() {
+                start = System.nanoTime()
+                val sleepTime = (Math.random() * 100).toLong()
+                try {
+                    // Sleep for a random amount of time to simulate real work.
+                    Thread.sleep(sleepTime)
+                } catch (exception: InterruptedException) {
+                    throw RuntimeException(exception)
+                } finally {
+                    end = System.nanoTime()
+                    latch.countDown()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
index 84b4abc..8bfee8e 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/taskexecutor/InstantWorkTaskExecutor.java
@@ -16,7 +16,6 @@
 
 package androidx.work.impl.utils.taskexecutor;
 
-import androidx.annotation.NonNull;
 import androidx.work.impl.utils.SynchronousExecutor;
 
 import java.util.concurrent.Executor;
@@ -47,10 +46,4 @@
     public Executor getBackgroundExecutor() {
         return mSynchronousExecutor;
     }
-
-    @NonNull
-    @Override
-    public Thread getBackgroundExecutorThread() {
-        return Thread.currentThread();
-    }
 }
diff --git a/work/workmanager/src/main/java/androidx/work/Configuration.java b/work/workmanager/src/main/java/androidx/work/Configuration.java
index 33c362b..1350bf4 100644
--- a/work/workmanager/src/main/java/androidx/work/Configuration.java
+++ b/work/workmanager/src/main/java/androidx/work/Configuration.java
@@ -49,6 +49,7 @@
     public static final int MIN_SCHEDULER_LIMIT = 20;
 
     private final @NonNull Executor mExecutor;
+    private final @NonNull Executor mTaskExecutor;
     private final @NonNull WorkerFactory mWorkerFactory;
     private final int mLoggingLevel;
     private final int mMinJobSchedulerId;
@@ -62,6 +63,15 @@
             mExecutor = builder.mExecutor;
         }
 
+        if (builder.mTaskExecutor == null) {
+            // This executor is used for *both* WorkManager's tasks and Room's query executor.
+            // So this should not be a single threaded executor. Writes will still be serialized
+            // as this will be wrapped with an SerialExecutor.
+            mTaskExecutor = createDefaultExecutor();
+        } else {
+            mTaskExecutor = builder.mTaskExecutor;
+        }
+
         if (builder.mWorkerFactory == null) {
             mWorkerFactory = WorkerFactory.getDefaultWorkerFactory();
         } else {
@@ -82,6 +92,14 @@
     }
 
     /**
+     * @return The {@link Executor} used by {@link WorkManager} for all its internal book-keeping.
+     */
+    @NonNull
+    public Executor getTaskExecutor() {
+        return mTaskExecutor;
+    }
+
+    /**
      * @return The {@link WorkerFactory} used by {@link WorkManager} to create
      *         {@link ListenableWorker}s
      */
@@ -151,6 +169,8 @@
 
         Executor mExecutor;
         WorkerFactory mWorkerFactory;
+        Executor mTaskExecutor;
+
         int mLoggingLevel = Log.INFO;
         int mMinJobSchedulerId = IdGenerator.INITIAL_ID;
         int mMaxJobSchedulerId = Integer.MAX_VALUE;
@@ -179,6 +199,25 @@
         }
 
         /**
+         * Specifies a {@link Executor} which will be used by WorkManager for all its
+         * internal book-keeping.
+         *
+         * For best performance this {@link Executor} should be bounded. This {@link Executor}
+         * should *not* be a single threaded executor.
+         *
+         * For more information look at
+         * {@link androidx.room.RoomDatabase.Builder#setQueryExecutor(Executor)}.
+         *
+         * @param taskExecutor The {@link Executor} which will be used by WorkManager for
+         *                             all its internal book-keeping
+         * @return This {@link Builder} instance
+         */
+        public @NonNull Builder setTaskExecutor(@NonNull Executor taskExecutor) {
+            mTaskExecutor = taskExecutor;
+            return this;
+        }
+
+        /**
          * Specifies the range of {@link android.app.job.JobInfo} IDs that can be used by
          * {@link WorkManager}.  WorkManager needs a range of at least {@code 1000} IDs.
          * <p>
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
index ec4f505..8a783c8 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
@@ -48,6 +48,7 @@
 import androidx.work.impl.model.WorkTagDao;
 import androidx.work.impl.model.WorkTypeConverters;
 
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -91,17 +92,23 @@
      * Creates an instance of the WorkDatabase.
      *
      * @param context         A context (this method will use the application context from it)
+     * @param queryExecutor   An {@link Executor} that will be used to execute all async Room
+     *                        queries.
      * @param useTestDatabase {@code true} to generate an in-memory database that allows main thread
      *                        access
      * @return The created WorkDatabase
      */
-    public static WorkDatabase create(Context context, boolean useTestDatabase) {
+    public static WorkDatabase create(
+            @NonNull Context context,
+            @NonNull Executor queryExecutor,
+            boolean useTestDatabase) {
         RoomDatabase.Builder<WorkDatabase> builder;
         if (useTestDatabase) {
             builder = Room.inMemoryDatabaseBuilder(context, WorkDatabase.class)
                     .allowMainThreadQueries();
         } else {
-            builder = Room.databaseBuilder(context, WorkDatabase.class, DB_NAME);
+            builder = Room.databaseBuilder(context, WorkDatabase.class, DB_NAME)
+                    .setQueryExecutor(queryExecutor);
         }
 
         return builder.addCallback(generateCleanupCallback())
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
index db19bdc..a500e80 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -179,7 +179,7 @@
                     sDefaultInstance = new WorkManagerImpl(
                             context,
                             configuration,
-                            new WorkManagerTaskExecutor());
+                            new WorkManagerTaskExecutor(configuration.getTaskExecutor()));
                 }
                 sDelegatedInstance = sDefaultInstance;
             }
@@ -224,7 +224,8 @@
             boolean useTestDatabase) {
 
         Context applicationContext = context.getApplicationContext();
-        WorkDatabase database = WorkDatabase.create(applicationContext, useTestDatabase);
+        WorkDatabase database = WorkDatabase.create(
+                applicationContext, configuration.getTaskExecutor(), useTestDatabase);
         Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
         List<Scheduler> schedulers = createSchedulers(applicationContext);
         Processor processor = new Processor(
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
index f482cf0..eb4e9b7 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -312,7 +312,6 @@
 
     // Package-private for synthetic accessor.
     void onWorkFinished() {
-        assertBackgroundExecutorThread();
         boolean isWorkFinished = false;
         if (!tryCheckForInterruptionAndResolve()) {
             try {
@@ -566,13 +565,6 @@
         }
     }
 
-    private void assertBackgroundExecutorThread() {
-        if (mWorkTaskExecutor.getBackgroundExecutorThread() != Thread.currentThread()) {
-            throw new IllegalStateException(
-                    "Needs to be executed on the Background executor thread.");
-        }
-    }
-
     private String createWorkDescription(List<String> tags) {
         StringBuilder sb = new StringBuilder("Work [ id=")
                 .append(mWorkSpecId)
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/SerialExecutor.java b/work/workmanager/src/main/java/androidx/work/impl/utils/SerialExecutor.java
new file mode 100644
index 0000000..90e9adc
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/SerialExecutor.java
@@ -0,0 +1,81 @@
+/*
+ * 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.work.impl.utils;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.Executor;
+
+/**
+ * A {@link Executor} which delegates to another {@link Executor} but ensures that tasks are
+ * executed serially, like a single threaded executor.
+ */
+public class SerialExecutor implements Executor {
+    private final ArrayDeque<Task> mTasks;
+    private final Executor mExecutor;
+    private final Object mLock;
+    private volatile Runnable mActive;
+
+    public SerialExecutor(@NonNull Executor executor) {
+        mExecutor = executor;
+        mTasks = new ArrayDeque<>();
+        mLock = new Object();
+    }
+
+    @Override
+    public void execute(@NonNull Runnable command) {
+        synchronized (mLock) {
+            mTasks.add(new Task(this, command));
+            if (mActive == null) {
+                scheduleNext();
+            }
+        }
+    }
+
+    // Synthetic access
+    void scheduleNext() {
+        synchronized (mLock) {
+            if ((mActive = mTasks.poll()) != null) {
+                mExecutor.execute(mActive);
+            }
+        }
+    }
+
+    /**
+     * A {@link Runnable} which tells the {@link SerialExecutor} to schedule the next command
+     * after completion.
+     */
+    static class Task implements Runnable {
+        final SerialExecutor mSerialExecutor;
+        final Runnable mRunnable;
+
+        Task(@NonNull SerialExecutor serialExecutor, @NonNull Runnable runnable) {
+            mSerialExecutor = serialExecutor;
+            mRunnable = runnable;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mRunnable.run();
+            } finally {
+                mSerialExecutor.scheduleNext();
+            }
+        }
+    }
+}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java b/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
index 056442d..e5df2d9 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
@@ -16,7 +16,6 @@
 
 package androidx.work.impl.utils.taskexecutor;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 
 import java.util.concurrent.Executor;
@@ -45,12 +44,6 @@
     void executeOnBackgroundThread(Runnable runnable);
 
     /**
-     * @return the {@link Thread} being used by WorkManager's background task executor.
-     */
-    @NonNull
-    Thread getBackgroundExecutorThread();
-
-    /**
      * @return The {@link Executor} for background task processing
      */
     Executor getBackgroundExecutor();
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java b/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
index ef2eb6a..d689d89 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
@@ -21,20 +21,25 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.work.impl.utils.SerialExecutor;
 
 import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
 
 /**
  * Default Task Executor for executing common tasks in WorkManager
  * @hide
  */
-
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class WorkManagerTaskExecutor implements TaskExecutor {
 
+    private final Executor mBackgroundExecutor;
+
+    public WorkManagerTaskExecutor(@NonNull Executor backgroundExecutor) {
+        // Wrap it with a serial executor so we have ordering guarantees on commands
+        // being executed.
+        mBackgroundExecutor = new SerialExecutor(backgroundExecutor);
+    }
+
     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
 
     private final Executor mMainThreadExecutor = new Executor() {
@@ -44,26 +49,6 @@
         }
     };
 
-    // Avoiding synthetic accessor.
-    volatile Thread mCurrentBackgroundExecutorThread;
-    private final ThreadFactory mBackgroundThreadFactory = new ThreadFactory() {
-
-        private int mThreadsCreated = 0;
-
-        @Override
-        public Thread newThread(@NonNull Runnable r) {
-            // Delegate to the default factory, but keep track of the current thread being used.
-            Thread thread = Executors.defaultThreadFactory().newThread(r);
-            thread.setName("WorkManager-WorkManagerTaskExecutor-thread-" + mThreadsCreated);
-            mThreadsCreated++;
-            mCurrentBackgroundExecutorThread = thread;
-            return thread;
-        }
-    };
-
-    private final ExecutorService mBackgroundExecutor =
-            Executors.newSingleThreadExecutor(mBackgroundThreadFactory);
-
     @Override
     public void postToMainThread(Runnable r) {
         mMainThreadHandler.post(r);
@@ -83,10 +68,4 @@
     public Executor getBackgroundExecutor() {
         return mBackgroundExecutor;
     }
-
-    @NonNull
-    @Override
-    public Thread getBackgroundExecutorThread() {
-        return mCurrentBackgroundExecutorThread;
-    }
 }